openapi: 3.1.0
info:
  title: prgd API
  version: "1"
  description: |
    The prgd public API. The same API powers the console, CLI, Terraform provider,
    SDKs and AI agents (via MCP in phase 2).

    **Conventions**
    - Base URL: `https://api.prgd.example/v1` (domain is an open decision)
    - Auth: `Authorization: Bearer prgd_…` (API token) or a console session JWT
    - Lists are cursor-paginated: `?limit=50&cursor=<id>` → `meta.next_cursor`
    - Mutations accept `Idempotency-Key` (replayed for 24h; same key + different body → 409)
    - Long operations return `202 Accepted` and a resource in a transitional status; poll or use webhooks
    - Money is integer minor units (`amountMinor`) with an explicit `currency` (`USD` | `SAR`)
    - Errors: `{ "error": { "code", "message", "details?" } }`
    - Rate limits: 600 requests per minute per token or session, 120 per minute per IP when anonymous,
      10 sign in attempts per minute per IP. Over the limit: `429 rate_limited` with `Retry-After`
servers:
  - url: http://localhost:4000/v1
    description: local development
security:
  - bearerAuth: []

tags:
  - name: auth
  - name: account
  - name: approvals
  - name: monitoring
  - name: servers
  - name: catalog
  - name: deploys
  - name: github
  - name: network
  - name: snapshots
  - name: volumes
  - name: load-balancers
  - name: dns
  - name: object-storage
  - name: databases
  - name: kubernetes
  - name: app-platform
  - name: marketplace
  - name: billing
  - name: support
  - name: managed-cloud
    description: Managed cloud contracts. Progrid engineers operate your servers and sites under an SLA. Contracts and reports are for team owners; members can open and read tickets and see assets.
  - name: webhooks

paths:
  /auth/signup:
    post:
      tags: [auth]
      security: []
      summary: Create a user, team and default project
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/Signup" } } } }
      responses:
        "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/Session" } } } }
        "409": { $ref: "#/components/responses/Conflict" }
  /auth/login:
    post:
      tags: [auth]
      security: []
      summary: Log in and receive a session token
      description: |
        When the user has two factor sign in, the first call answers 401 `totp_required`; repeat it with `totp`
        set to the six digit authenticator code or an unused recovery code. Team owners must enable two factor
        when `REQUIRE_TOTP_FOR_OWNERS` is on; until then console sessions get 403 `totp_setup_required` on every
        route except the account and two factor endpoints. API tokens are exempt.
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [email, password], properties: { email: { type: string, format: email }, password: { type: string }, totp: { type: string } } } } } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Session" } } } }
        "401": { description: "Wrong email or password, `totp_required`, or `password_not_set` for an account that signs in only with Google or Microsoft (`details.providers` names them)" }

  /auth/verify:
    post:
      tags: [auth]
      security: []
      summary: Confirm an email address with the token from the verification email
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [token], properties: { token: { type: string } } } } } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { verified: { type: boolean } } } } } }
        "400": { description: Token invalid or expired (`token_invalid`) }
  /auth/verify/request:
    post:
      tags: [auth]
      summary: Send the verification email again for the signed in user
      responses: { "204": { description: Sent (or already verified) } }
  /auth/password/forgot:
    post:
      tags: [auth]
      security: []
      summary: Send a password reset link
      description: Always returns 204 so email addresses cannot be probed. Limited to 5 requests per 10 minutes per IP.
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [email], properties: { email: { type: string, format: email } } } } } }
      responses: { "204": { description: Accepted } }
  /auth/password/reset:
    post:
      tags: [auth]
      security: []
      summary: Set a new password with the token from the reset email
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [token, password], properties: { token: { type: string }, password: { type: string, minLength: 10 } } } } } }
      responses:
        "200": { description: OK }
        "400": { description: Token invalid or expired (`token_invalid`) }
  /auth/totp/setup:
    post:
      tags: [auth]
      summary: Start two factor enrollment
      description: Returns a new secret and an `otpauth://` URL to show as a QR code. Nothing is enforced until `/auth/totp/enable` confirms a code.
      responses:
        "201": { description: OK, content: { application/json: { schema: { type: object, properties: { secret: { type: string }, otpauthUrl: { type: string } } } } } }
        "409": { description: Already enabled (`totp_enabled`) }
  /auth/totp/enable:
    post:
      tags: [auth]
      summary: Confirm the authenticator and turn two factor on
      description: Returns ten recovery codes exactly once. Each recovery code signs the user in a single time.
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/TotpCode" } } } }
      responses:
        "201": { description: OK, content: { application/json: { schema: { type: object, properties: { enabled: { type: boolean }, recoveryCodes: { type: array, items: { type: string } } } } } } }
        "401": { description: Code not valid (`totp_invalid`) }
  /auth/totp/disable:
    post:
      tags: [auth]
      summary: Turn two factor off (needs a current code or a recovery code)
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/TotpCode" } } } }
      responses:
        "201": { description: OK }
        "401": { description: Code not valid (`totp_invalid`) }

  /auth/providers:
    get:
      tags: [auth]
      security: []
      summary: Social sign in providers that are configured (the console shows a button for each)
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { providers: { type: array, items: { type: object, properties: { id: { type: string, enum: [google, microsoft] }, name: { type: string } } } } } } } } }
  /auth/oauth/{provider}/start:
    get:
      tags: [auth]
      security: []
      summary: Start sign in with Google or Microsoft (the browser navigates here)
      description: |
        Redirects to the provider with the authorization code flow, PKCE (S256), `state` and `nonce`, scopes
        `openid email profile`. Sets an HttpOnly, Secure, SameSite=Lax cookie that binds the sign in to this
        browser; the pending state lives on the server for 10 minutes. `intent=link` needs a `ticket` from
        `POST /auth/oauth/link-ticket`. Errors redirect to the console `/auth/callback?error=<code>`.
      parameters:
        - { name: provider, in: path, required: true, schema: { type: string, enum: [google, microsoft] } }
        - { name: intent, in: query, schema: { type: string, enum: [login, signup, link], default: login } }
        - { name: return, in: query, description: Console path to land on afterwards, schema: { type: string } }
        - { name: invite, in: query, description: Invitation token; a new account joins that team instead of creating one, schema: { type: string } }
        - { name: ticket, in: query, description: One time link ticket (intent link), schema: { type: string } }
        - { name: locale, in: query, schema: { type: string, enum: [en, tr, ar] } }
      responses:
        "302": { description: Redirect to the provider, or to the console with an error code }
  /auth/oauth/{provider}/callback:
    get:
      tags: [auth]
      security: []
      summary: Provider redirect URI
      description: |
        Checks `state` and the browser cookie, exchanges the code with the PKCE verifier, verifies the id token
        (signature against the provider key set, audience, issuer, nonce) and redirects to the console
        `/auth/callback?code=<one time code>` (valid 60 seconds). Session tokens never appear in URLs.
        Error codes: `state_invalid`, `cancelled`, `provider_error`, `token_invalid`, `link_from_security`,
        `identity_in_use`, `email_missing`, `account_exists`, `invite_email_mismatch`, `invite_invalid`,
        `link_session_invalid`.
      parameters:
        - { name: provider, in: path, required: true, schema: { type: string, enum: [google, microsoft] } }
      responses:
        "302": { description: Redirect to the console }
  /auth/oauth/exchange:
    post:
      tags: [auth]
      security: []
      summary: Redeem the one time code from the console callback
      description: Same answer as `/auth/login`, plus `created` and `returnTo`. Accounts with two factor sign in get `{ totpRequired, ticket }` instead; finish with `/auth/oauth/totp`.
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [code], properties: { code: { type: string } } } } } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { oneOf: [{ $ref: "#/components/schemas/Session" }, { type: object, properties: { totpRequired: { type: boolean, const: true }, ticket: { type: string } } }] } } } }
        "400": { description: Code unknown, expired or already used (`code_invalid`) }
        "403": { description: Staff account without two factor sign in (`totp_setup_required`) }
  /auth/oauth/totp:
    post:
      tags: [auth]
      security: []
      summary: Second factor for a social sign in
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [ticket, code], properties: { ticket: { type: string }, code: { type: string } } } } } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Session" } } } }
        "400": { description: Ticket expired or used (`ticket_invalid`); five wrong codes end it }
        "401": { description: Code not valid (`totp_invalid`) }
  /auth/oauth/link-ticket:
    post:
      tags: [auth]
      summary: One time ticket (60 seconds) to link Google or Microsoft to the signed in user
      description: Console sessions only; API tokens get 403. Pass it to `/auth/oauth/{provider}/start?intent=link&ticket=…`.
      responses:
        "201": { description: OK, content: { application/json: { schema: { type: object, properties: { ticket: { type: string } } } } } }
  /account/identities:
    get:
      tags: [account]
      summary: Linked Google and Microsoft accounts, whether a password is set, and which providers can be linked
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { hasPassword: { type: boolean }, available: { type: array, items: { type: string } }, data: { type: array, items: { type: object, properties: { id: { type: string }, provider: { type: string, enum: [google, microsoft] }, email: { type: string }, emailVerified: { type: boolean }, tenantId: { type: [string, "null"] }, createdAt: { type: string, format: date-time }, lastUsedAt: { type: [string, "null"], format: date-time } } } } } } } } }
  /account/identities/{id}:
    delete:
      tags: [account]
      summary: Unlink a Google or Microsoft account
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        "204": { description: Unlinked }
        "409": { description: It is the only way left to sign in (`last_sign_in_method`) }

  /account:
    get:
      tags: [account]
      summary: Current user, team, role and scopes
      responses:
        "200": { description: OK }
  /projects:
    get: { tags: [account], summary: List projects, responses: { "200": { description: OK } } }
    post:
      tags: [account]
      summary: Create a project
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [name, slug], properties: { name: { type: string }, slug: { type: string, pattern: "^[a-z0-9-]{2,40}$" }, spendLimitMinor: { type: integer } } } } } }
      responses: { "201": { description: Created } }
  /tokens:
    get: { tags: [account], summary: List API tokens, responses: { "200": { description: OK } } }
    post:
      tags: [account]
      summary: Create an API token (human or AI agent)
      description: |
        Agent-safe tokens set `isAgent: true` and may carry `spendCapMinor` (monthly hard cap)
        and `requireApprovalFor` (e.g. `["servers:delete","servers:resize-down"]`). A token can
        never carry scopes its creator does not have.
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/CreateToken" } } } }
      responses:
        "201": { description: "Created — `token` is shown once", content: { application/json: { schema: { type: object, properties: { id: { type: string }, token: { type: string }, prefix: { type: string } } } } } }
  /tokens/{id}:
    delete: { tags: [account], summary: Revoke a token, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "204": { description: Revoked } } }
  /audit:
    get: { tags: [account], summary: Team audit log (newest 200 entries), responses: { "200": { description: OK } } }
  /interest:
    post:
      tags: [account]
      summary: Register interest in a roadmap product ("notify me when this launches")
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [product], properties: { product: { type: string, example: inference }, note: { type: string } } } } } }
      responses: { "204": { description: Recorded } }
  /ssh-keys:
    get: { tags: [account], summary: List SSH keys, responses: { "200": { description: OK } } }
    post:
      tags: [account]
      summary: Add an SSH public key
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [name, publicKey], properties: { name: { type: string }, publicKey: { type: string } } } } } }
      responses: { "201": { description: Created } }
  /ssh-keys/{id}:
    delete: { tags: [account], summary: Delete an SSH key, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "204": { description: Deleted } } }

  /regions:
    get: { tags: [catalog], security: [], summary: List regions, responses: { "200": { description: OK } } }
  /sizes:
    get: { tags: [catalog], security: [], summary: List server sizes, responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/Size" } } } } } } } } }
  /images:
    get:
      tags: [catalog]
      security: []
      summary: List images
      parameters: [{ name: kind, in: query, schema: { type: string, enum: [distribution, marketplace] } }]
      responses: { "200": { description: OK } }
  /billing/topup:
    post:
      tags: [billing]
      summary: Start a card payment that becomes prepaid credit
      description: Returns the hosted Moyasar payment page (mada, Visa, Mastercard, Apple Pay) to send the person to. Agents cannot call this.
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [amountMinor], properties: { amountMinor: { type: integer, description: "USD 5 to 5000, SAR 200 to 200000" } } } } } }
      responses:
        "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/Checkout" } } } }
  /billing/invoices/{id}/pay:
    post:
      tags: [billing]
      summary: Pay an open invoice by card
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/Checkout" } } } }
        "409": { description: Invoice is not open (`invalid_state`) }
  /billing/invoices/{id}/pdf:
    get:
      tags: [billing]
      summary: Invoice as PDF
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: PDF, content: { application/pdf: { schema: { type: string, format: binary } } } } }
  /billing/payments:
    get: { tags: [billing], summary: Card payments of the team, responses: { "200": { description: OK } } }
  /billing/payments/moyasar/webhook:
    post: { tags: [billing], security: [], summary: Moyasar webhook (payment events, verified with the shared token and an invoice fetch), responses: { "200": { description: OK } } }
  /billing/payments/moyasar/callback:
    get: { tags: [billing], security: [], summary: Moyasar callback after the hosted page (result verified by retrieving the invoice), responses: { "302": { description: Redirect to the console } } }
  /pricing:
    get:
      tags: [catalog]
      security: []
      summary: Public price list (USD base; SAR converted at the current exchange rate)
      parameters: [{ name: currency, in: query, schema: { type: string, enum: [USD, SAR], default: USD } }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  currency: { type: string }
                  baseCurrency: { type: string, enum: [USD] }
                  fxRate: { type: number, description: USD to currency rate used for this response }
                  hoursPerMonth: { type: integer }
                  data: { type: array, items: { type: object, properties: { resourceType: { type: string }, sku: { type: string }, unit: { type: string }, monthlyMinor: { type: integer }, hourlyMinor: { type: integer }, size: { $ref: "#/components/schemas/Size" } } } }

  /servers:
    get:
      tags: [servers]
      summary: List servers
      parameters:
        - { $ref: "#/components/parameters/project" }
        - { $ref: "#/components/parameters/limit" }
        - { $ref: "#/components/parameters/cursor" }
        - { name: status, in: query, schema: { $ref: "#/components/schemas/ServerStatus" } }
        - { name: tag, in: query, schema: { type: string } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/Server" } }, meta: { $ref: "#/components/schemas/PageMeta" } } } } } }
    post:
      tags: [servers]
      summary: Create a server
      description: |
        Returns `202` immediately with `status: new`. A durable workflow places the server,
        reserves a public IP, clones the image, waits for cloud-init, applies the firewall and
        starts metering. Watch `status` become `active` (30–60 s) or subscribe to `server.active`.
        The server is never billed unless it reaches `active`.
      parameters: [{ $ref: "#/components/parameters/idempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/CreateServer" } } } }
      responses:
        "202": { description: Accepted, content: { application/json: { schema: { $ref: "#/components/schemas/Server" } } } }
        "402": { description: Spend limit reached (`spend_limit_reached`) }
        "403": { description: Quota exceeded (`quota_exceeded`), account not verified (`verification_required`) or suspended }
        "422": { $ref: "#/components/responses/Invalid" }
  /servers/{id}:
    get:
      tags: [servers]
      summary: Get a server
      parameters: [{ $ref: "#/components/parameters/id" }]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Server" } } } }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [servers]
      summary: Delete a server
      description: Returns `202`; the server moves to `deleting` and is removed by a workflow. Metering stops immediately.
      parameters: [{ $ref: "#/components/parameters/id" }, { $ref: "#/components/parameters/idempotencyKey" }]
      responses:
        "202": { description: Accepted, content: { application/json: { schema: { $ref: "#/components/schemas/ServerAction" } } } }
        "409": { description: Invalid state (`invalid_state`) }
    patch:
      tags: [servers]
      summary: Rename, retag, or turn backups or the managed tier on or off
      description: Turning managed on also turns backups on. A server that never had the care agent gets it on its next rebuild, or right away with the install command from `GET /servers/{id}/managed`.
      parameters: [{ $ref: "#/components/parameters/id" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { name: { type: string }, tags: { type: array, items: { type: string } }, backups: { type: boolean, description: Daily platform snapshot, last seven kept, 20 percent of the plan }, managed: { type: boolean, description: Managed tier, 30 percent of the plan plus backups } } } } } }
      responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Server" } } } } }
  /servers/{id}/managed:
    get:
      tags: [servers]
      summary: Managed tier status
      description: Health from the care agent's last report, the report itself, and the install command while the agent is not reporting.
      parameters: [{ $ref: "#/components/parameters/id" }]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ManagedStatus" } } } }
  /managed/install/{token}:
    get:
      tags: [servers]
      summary: Care agent install script
      description: Public, keyed by the server's managed token. Served as a shell script for `curl | sh`.
      security: []
      parameters: [{ name: token, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: OK, content: { text/plain: { schema: { type: string } } } }, "401": { description: Unknown token } }
  /managed/report:
    post:
      tags: [servers]
      summary: Care agent report
      description: Posted by the agent inside a managed server every five minutes, authenticated by the `X-Prgd-Managed-Token` header.
      security: []
      parameters: [{ name: X-Prgd-Managed-Token, in: header, required: true, schema: { type: string } }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/ManagedReport" } } } }
      responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { ok: { type: boolean }, health: { type: string }, issues: { type: array, items: { type: string } } } } } } }, "401": { description: Unknown token } }
  /servers/{id}/actions:
    get:
      tags: [servers]
      summary: List recent actions on a server
      parameters: [{ $ref: "#/components/parameters/id" }]
      responses: { "200": { description: OK } }
    post:
      tags: [servers]
      summary: Run a lifecycle action
      parameters: [{ $ref: "#/components/parameters/id" }, { $ref: "#/components/parameters/idempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type]
              properties:
                type: { type: string, enum: [start, stop, reboot, resize, rebuild, snapshot] }
                size: { type: string, description: "resize: target size id (disk cannot shrink)" }
                image: { type: string, description: "rebuild: image id (defaults to current)" }
                name: { type: string, description: "snapshot: name" }
                force: { type: boolean, description: "stop: power off without ACPI shutdown" }
      responses:
        "202": { description: Accepted, content: { application/json: { schema: { $ref: "#/components/schemas/ServerAction" } } } }
        "409": { description: Invalid state (`invalid_state`) }

  /deploys:
    get: { tags: [deploys], summary: List Git deployments, parameters: [{ $ref: "#/components/parameters/project" }], responses: { "200": { description: OK } } }
    post:
      tags: [deploys]
      summary: Deploy a Git repository onto a new server
      description: |
        Creates a server whose cloud-init installs Docker, clones the repo, builds (Dockerfile or docker-compose) and serves it on :80.
        Two ways to name the repository: `repoUrl` (public, or private with `gitToken`) returns a per deployment GitHub webhook URL and
        secret (shown once) to add to the repository; `installationId` + `repo` ("owner/name") uses the team's GitHub App installation,
        needs no token, and redeploys on every push through the app webhook (`webhook` is null).
      parameters: [{ $ref: "#/components/parameters/idempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { repoUrl: { type: string, format: uri }, installationId: { type: string }, repo: { type: string, example: acme/app }, branch: { type: string, default: main }, name: { type: string }, port: { type: integer, default: 3000 }, size: { type: string }, project: { type: string }, sshKeys: { type: array, items: { type: string } }, env: { type: object, additionalProperties: { type: string } }, gitToken: { type: string, description: For private repos; stored only in the server's cloud-init } } } } } }
      responses: { "202": { description: Accepted; includes `webhook` (url and secret) for URL based deployments, null for app based ones } }
  /deploys/{id}:
    get: { tags: [deploys], summary: Get a deployment (refreshes status from the server), parameters: [{ $ref: "#/components/parameters/id" }], responses: { "200": { description: OK } } }
  /deploys/{id}/redeploy:
    post: { tags: [deploys], summary: Redeploy now, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "202": { description: Accepted } } }
  /deploys/{id}/hook:
    post:
      tags: [deploys]
      security: []
      summary: GitHub push webhook (verified with X-Hub-Signature-256)
      parameters: [{ $ref: "#/components/parameters/id" }, { name: X-Hub-Signature-256, in: header, required: true, schema: { type: string } }, { name: X-GitHub-Event, in: header, schema: { type: string } }]
      responses: { "200": { description: OK }, "401": { $ref: "#/components/responses/Unauthorized" } }
  /approvals:
    get:
      tags: [approvals]
      summary: List approval requests (agents see only their own)
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [pending, approved, denied, expired, failed] } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/Approval" } }, pending: { type: integer } } } } } }
  /approvals/{id}:
    get:
      tags: [approvals]
      summary: Get one approval request
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Approval" } } } }
        "404": { $ref: "#/components/responses/NotFound" }
  /approvals/{id}/approve:
    post:
      tags: [approvals]
      summary: Approve and run the parked request
      description: Team owners and admins only, never an agent token. The request runs with the agent token's project scope and spending cap still applied; the audit log records both the person and the token.
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        "201": { description: Ran, content: { application/json: { schema: { $ref: "#/components/schemas/Approval" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { description: Already decided or expired (`invalid_state`) }
  /approvals/{id}/deny:
    post:
      tags: [approvals]
      summary: Deny the parked request
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      requestBody: { content: { application/json: { schema: { type: object, properties: { reason: { type: string, maxLength: 500 } } } } } }
      responses:
        "201": { description: Denied, content: { application/json: { schema: { $ref: "#/components/schemas/Approval" } } } }
  /deploys/{id}/logs:
    get:
      tags: [deploys]
      summary: Tail of the last build log
      description: Fetched live from the server when it is reachable, otherwise the last cached copy. Capped at 32 KB.
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { id: { type: string }, status: { type: string }, commit: { type: string, nullable: true }, log: { type: string }, updatedAt: { type: string, format: date-time, nullable: true }, live: { type: boolean, description: true when read from the server just now } } } } } }
  /github/app:
    get: { tags: [github], summary: Whether the GitHub App integration is configured, responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { enabled: { type: boolean } } } } } } } }
  /github/connect:
    get:
      tags: [github]
      summary: URL to install the GitHub App for this team
      description: Send the user there. GitHub returns them to the console callback with installation_id and the signed state, which the console posts to /github/installations.
      responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { url: { type: string } } } } } }, "503": { description: Not configured (`github_app_unavailable`) } }
  /github/installations:
    get: { tags: [github], summary: GitHub App installations linked to the team, responses: { "200": { description: OK } } }
    post:
      tags: [github]
      summary: Link an installation after the GitHub redirect
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [installationId, state], properties: { installationId: { type: integer }, state: { type: string } } } } } }
      responses: { "201": { description: Linked }, "401": { description: State invalid or expired } }
  /github/installations/{id}:
    delete: { tags: [github], summary: Unlink and uninstall, parameters: [{ name: id, in: path, required: true, schema: { type: string } }], responses: { "204": { description: Removed } } }
  /github/installations/{id}/repos:
    get: { tags: [github], summary: Repositories the installation can reach, parameters: [{ name: id, in: path, required: true, schema: { type: string } }], responses: { "200": { description: OK } } }
  /github/webhook:
    post:
      tags: [github]
      security: []
      summary: GitHub App webhook (push, installation)
      description: One URL for every installation, verified with X-Hub-Signature-256 and the app webhook secret. A push redeploys every deployment on that repository and branch.
      responses: { "200": { description: OK } }
  /servers/{id}/metrics:
    get:
      tags: [monitoring]
      summary: Time series for a server
      description: One point per minute for 1h, 6h and 24h; hourly averages (with the hour's CPU peak) for 7d and 30d. Network and disk are bytes per second; multiply by 8 and divide by a million for Mbps.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: period, in: query, schema: { type: string, enum: [1h, 6h, 24h, 7d, 30d], default: 1h } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/MetricSeries" } } } }
  /alerts:
    get: { tags: [monitoring], summary: List alert rules, responses: { "200": { description: OK } } }
    post:
      tags: [monitoring]
      summary: Create an alert rule
      description: Fires when the metric, averaged over windowMinutes, is above or below the threshold on any matching server. Empty serverIds and tags means every server in the team. Owners and admins are emailed, plus any addresses in emails; webhooks get alert.triggered and alert.resolved.
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/AlertRuleInput" } } } }
      responses: { "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/AlertRule" } } } } }
  /alerts/incidents:
    get:
      tags: [monitoring]
      summary: Incidents (open and recent)
      parameters: [{ name: open, in: query, schema: { type: boolean } }]
      responses: { "200": { description: OK } }
  /alerts/{id}:
    get: { tags: [monitoring], summary: Get an alert rule with recent incidents, parameters: [{ name: id, in: path, required: true, schema: { type: string } }], responses: { "200": { description: OK }, "404": { $ref: "#/components/responses/NotFound" } } }
    patch:
      tags: [monitoring]
      summary: Update an alert rule (any field, including enabled to mute)
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/AlertRuleInput" } } } }
      responses: { "200": { description: OK } }
    delete: { tags: [monitoring], summary: Delete an alert rule, parameters: [{ name: id, in: path, required: true, schema: { type: string } }], responses: { "204": { description: Deleted } } }
  /firewalls:
    get: { tags: [network], summary: List firewalls, parameters: [{ $ref: "#/components/parameters/project" }], responses: { "200": { description: OK } } }
    post:
      tags: [network]
      summary: Create a firewall
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [name, rules], properties: { name: { type: string }, project: { type: string }, rules: { type: array, items: { $ref: "#/components/schemas/FirewallRule" } } } } } } }
      responses: { "201": { description: Created } }
  /firewalls/{id}:
    get: { tags: [network], summary: Get a firewall, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "200": { description: OK } } }
    delete: { tags: [network], summary: Delete a firewall, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "204": { description: Deleted } } }
  /firewalls/{id}/rules:
    put:
      tags: [network]
      summary: Replace all rules (re-applied to attached servers immediately)
      parameters: [{ $ref: "#/components/parameters/id" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [rules], properties: { rules: { type: array, items: { $ref: "#/components/schemas/FirewallRule" } } } } } } }
      responses: { "200": { description: OK } }
  /firewalls/{id}/servers:
    post:
      tags: [network]
      summary: Attach a server
      parameters: [{ $ref: "#/components/parameters/id" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [serverId], properties: { serverId: { type: string } } } } } }
      responses: { "204": { description: Attached } }
  /firewalls/{id}/servers/{serverId}:
    delete: { tags: [network], summary: Detach a server, parameters: [{ $ref: "#/components/parameters/id" }, { name: serverId, in: path, required: true, schema: { type: string } }], responses: { "204": { description: Detached } } }
  /public-ips:
    get: { tags: [network], summary: List public IPs in a project, parameters: [{ $ref: "#/components/parameters/project" }], responses: { "200": { description: OK } } }

  /snapshots:
    get: { tags: [snapshots], summary: List snapshots, parameters: [{ $ref: "#/components/parameters/project" }], responses: { "200": { description: OK } } }
  /snapshots/{id}:
    delete: { tags: [snapshots], summary: Delete a snapshot, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "202": { description: Accepted } } }

  /volumes:
    get: { tags: [volumes], summary: List volumes, parameters: [{ $ref: "#/components/parameters/project" }, { name: server, in: query, description: Only volumes attached to this server, schema: { type: string } }], responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/Volume" } } } } } } } } }
    post:
      tags: [volumes]
      summary: Create a volume
      description: Allocates a block volume in the region. Returns 202; the volume becomes `available` within seconds, or `attached` when `serverId` is given.
      parameters: [{ $ref: "#/components/parameters/idempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/CreateVolume" } } } }
      responses: { "202": { description: Accepted, content: { application/json: { schema: { $ref: "#/components/schemas/Volume" } } } }, "403": { $ref: "#/components/responses/Forbidden" } }
  /volumes/{id}:
    get: { tags: [volumes], summary: Get a volume, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Volume" } } } }, "404": { $ref: "#/components/responses/NotFound" } } }
    delete: { tags: [volumes], summary: Delete a volume, description: The volume must be detached. Data is gone for good., parameters: [{ $ref: "#/components/parameters/id" }], responses: { "202": { description: Accepted }, "409": { description: Still attached } } }
  /volumes/{id}/attach:
    post: { tags: [volumes], summary: Attach to a server, description: Hot plugs the volume. The server must be in the same region and active or off. The guest sees the disk at `device`., parameters: [{ $ref: "#/components/parameters/id" }], requestBody: { required: true, content: { application/json: { schema: { type: object, required: [serverId], properties: { serverId: { type: string } } } } } }, responses: { "202": { description: Accepted, content: { application/json: { schema: { $ref: "#/components/schemas/Volume" } } } } } }
  /volumes/{id}/detach:
    post: { tags: [volumes], summary: Detach from its server, description: Unmount the file system in the guest first., parameters: [{ $ref: "#/components/parameters/id" }], responses: { "202": { description: Accepted, content: { application/json: { schema: { $ref: "#/components/schemas/Volume" } } } } } }
  /volumes/{id}/resize:
    post: { tags: [volumes], summary: Grow a volume, description: Volumes only grow. An attached volume grows live; extend the file system in the guest afterwards., parameters: [{ $ref: "#/components/parameters/id" }], requestBody: { required: true, content: { application/json: { schema: { type: object, required: [sizeGb], properties: { sizeGb: { type: integer, minimum: 10, maximum: 16384 } } } } } }, responses: { "202": { description: Accepted, content: { application/json: { schema: { $ref: "#/components/schemas/Volume" } } } } } }

  /load-balancers:
    get: { tags: [load-balancers], summary: List load balancers, parameters: [{ $ref: "#/components/parameters/project" }], responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/LoadBalancer" } } } } } } } } }
    post:
      tags: [load-balancers]
      summary: Create a load balancer
      description: Reserves a public IP, creates one to three HAProxy nodes and pushes the forwarding rules. Returns 202; status becomes `active` once every node runs the config.
      parameters: [{ $ref: "#/components/parameters/idempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/CreateLoadBalancer" } } } }
      responses: { "202": { description: Accepted, content: { application/json: { schema: { $ref: "#/components/schemas/LoadBalancer" } } } }, "403": { $ref: "#/components/responses/Forbidden" } }
  /load-balancers/{id}:
    get: { tags: [load-balancers], summary: Get a load balancer with target health, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/LoadBalancer" } } } }, "404": { $ref: "#/components/responses/NotFound" } } }
    patch: { tags: [load-balancers], summary: Change rules, health check, algorithm, sticky sessions or tag, parameters: [{ $ref: "#/components/parameters/id" }], requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/UpdateLoadBalancer" } } } }, responses: { "202": { description: Accepted, content: { application/json: { schema: { $ref: "#/components/schemas/LoadBalancer" } } } } } }
    delete: { tags: [load-balancers], summary: Delete a load balancer, description: Deletes the nodes and releases the IP., parameters: [{ $ref: "#/components/parameters/id" }], responses: { "202": { description: Accepted } } }
  /load-balancers/{id}/servers:
    post: { tags: [load-balancers], summary: Add target servers, parameters: [{ $ref: "#/components/parameters/id" }], requestBody: { required: true, content: { application/json: { schema: { type: object, required: [serverIds], properties: { serverIds: { type: array, items: { type: string } } } } } } }, responses: { "202": { description: Accepted, content: { application/json: { schema: { $ref: "#/components/schemas/LoadBalancer" } } } } } }
  /load-balancers/{id}/servers/{serverId}:
    delete: { tags: [load-balancers], summary: Remove a target server, parameters: [{ $ref: "#/components/parameters/id" }, { name: serverId, in: path, required: true, schema: { type: string } }], responses: { "202": { description: Accepted } } }
  /certificates:
    get: { tags: [load-balancers], summary: List certificates, parameters: [{ $ref: "#/components/parameters/project" }], responses: { "200": { description: OK } } }
    post:
      tags: [load-balancers]
      summary: Add a certificate
      description: Either an uploaded PEM pair (`custom`) or a list of domains the load balancer gets issued by Let's Encrypt (`letsencrypt`). Point the domains at the load balancer IP first.
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [name, type], properties: { name: { type: string }, type: { type: string, enum: [custom, letsencrypt] }, certPem: { type: string }, keyPem: { type: string }, domains: { type: array, items: { type: string } }, project: { type: string } } } } } }
      responses: { "201": { description: Created } }
  /certificates/{id}:
    delete: { tags: [load-balancers], summary: Delete a certificate, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "200": { description: Deleted }, "409": { description: In use by a load balancer } } }

  /domains:
    get: { tags: [dns], summary: List hosted zones, parameters: [{ $ref: "#/components/parameters/project" }], responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/Domain" } }, nameservers: { type: array, items: { type: string } } } } } } } } }
    post:
      tags: [dns]
      summary: Add a domain
      description: Creates the zone on our nameservers. Set the domain's nameservers at the registrar to the ones returned. Free.
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [name], properties: { name: { type: string, example: example.com }, ip: { type: string, description: Creates an apex A record }, project: { type: string } } } } } }
      responses: { "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/Domain" } } } }, "409": { description: Name taken } }
  /domains/{name}:
    get: { tags: [dns], summary: Get a zone with its records, parameters: [{ name: name, in: path, required: true, schema: { type: string } }], responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Domain" } } } }, "404": { $ref: "#/components/responses/NotFound" } } }
    delete: { tags: [dns], summary: Delete a zone and its records, parameters: [{ name: name, in: path, required: true, schema: { type: string } }], responses: { "200": { description: Deleted } } }
  /domains/{name}/zone-file:
    get: { tags: [dns], summary: The zone in BIND format, parameters: [{ name: name, in: path, required: true, schema: { type: string } }], responses: { "200": { description: OK, content: { text/plain: { schema: { type: string } } } } } }
  /domains/{name}/records:
    post:
      tags: [dns]
      summary: Add a record
      parameters: [{ name: name, in: path, required: true, schema: { type: string } }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/CreateDnsRecord" } } } }
      responses: { "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/DnsRecord" } } } }, "409": { description: CNAME conflict } }
  /domains/{name}/records/{id}:
    patch: { tags: [dns], summary: Change a record, parameters: [{ name: name, in: path, required: true, schema: { type: string } }, { $ref: "#/components/parameters/id" }], requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { name: { type: string }, content: { type: string }, ttl: { type: integer }, priority: { type: integer } } } } } }, responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/DnsRecord" } } } } } }
    delete: { tags: [dns], summary: Delete a record, parameters: [{ name: name, in: path, required: true, schema: { type: string } }, { $ref: "#/components/parameters/id" }], responses: { "200": { description: Deleted } } }
  /public-ips/{id}/reverse-dns:
    put: { tags: [dns], summary: Set reverse DNS (PTR) for a public IP, parameters: [{ $ref: "#/components/parameters/id" }], requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { name: { type: string, nullable: true, description: Hostname, or null to clear } } } } } }, responses: { "200": { description: OK } } }

  /buckets:
    get: { tags: [object-storage], summary: List buckets, parameters: [{ $ref: "#/components/parameters/project" }], responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/Bucket" } }, endpoint: { type: string }, region: { type: string } } } } } } } }
    post:
      tags: [object-storage]
      summary: Create a bucket
      description: Names are global and DNS safe (3 to 63 lowercase letters, digits, hyphens). Billed per GB per month.
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [name], properties: { name: { type: string }, region: { type: string }, project: { type: string }, public: { type: boolean, description: Anyone can read objects } } } } } }
      responses: { "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/Bucket" } } } }, "409": { description: Name taken } }
  /buckets/{name}:
    get: { tags: [object-storage], summary: Get a bucket, parameters: [{ name: name, in: path, required: true, schema: { type: string } }], responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Bucket" } } } }, "404": { $ref: "#/components/responses/NotFound" } } }
    patch: { tags: [object-storage], summary: Change public read, parameters: [{ name: name, in: path, required: true, schema: { type: string } }], requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { public: { type: boolean } } } } } }, responses: { "200": { description: OK } } }
    delete: { tags: [object-storage], summary: Delete an empty bucket, parameters: [{ name: name, in: path, required: true, schema: { type: string } }], responses: { "200": { description: Deleted }, "409": { description: Not empty } } }
  /buckets/{name}/objects:
    get: { tags: [object-storage], summary: List objects under a prefix, parameters: [{ name: name, in: path, required: true, schema: { type: string } }, { name: prefix, in: query, schema: { type: string } }, { name: token, in: query, schema: { type: string } }], responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { prefix: { type: string }, prefixes: { type: array, items: { type: string } }, objects: { type: array, items: { type: object, properties: { key: { type: string }, size: { type: integer }, lastModified: { type: string } } } }, nextToken: { type: string } } } } } } } }
    delete: { tags: [object-storage], summary: Delete one object, parameters: [{ name: name, in: path, required: true, schema: { type: string } }, { name: key, in: query, required: true, schema: { type: string } }], responses: { "200": { description: Deleted } } }
  /buckets/{name}/presign:
    post: { tags: [object-storage], summary: Presigned URL for GET, PUT or DELETE of one key, parameters: [{ name: name, in: path, required: true, schema: { type: string } }], requestBody: { required: true, content: { application/json: { schema: { type: object, required: [key], properties: { key: { type: string }, method: { type: string, enum: [GET, PUT, DELETE], default: GET }, expiresSeconds: { type: integer, minimum: 60, maximum: 604800, default: 900 }, contentType: { type: string } } } } } }, responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { url: { type: string }, method: { type: string }, key: { type: string }, expiresAt: { type: string, format: date-time } } } } } } } }
  /storage-keys:
    get: { tags: [object-storage], summary: List S3 access keys (no secrets), parameters: [{ $ref: "#/components/parameters/project" }], responses: { "200": { description: OK } } }
    post: { tags: [object-storage], summary: Create an access key; the secret is returned once, requestBody: { required: true, content: { application/json: { schema: { type: object, required: [name], properties: { name: { type: string }, project: { type: string } } } } } }, responses: { "201": { description: Created, content: { application/json: { schema: { type: object, properties: { id: { type: string }, name: { type: string }, accessKey: { type: string }, secretKey: { type: string }, endpoint: { type: string }, region: { type: string } } } } } } } }
  /storage-keys/{id}:
    delete: { tags: [object-storage], summary: Revoke an access key, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "200": { description: Revoked } } }

  /app-platform/sizes:
    get: { tags: [app-platform], summary: Container sizes, responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { type: object, properties: { id: { type: string }, memoryMb: { type: integer }, cpus: { type: number } } } } } } } } } } }
  /app-platform/apps:
    get: { tags: [app-platform], summary: List apps, parameters: [{ $ref: "#/components/parameters/project" }], responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/PlatformApp" } } } } } } } } }
    post:
      tags: [app-platform]
      summary: Create an app
      description: |
        Push code, get a URL. Returns `202` with status `creating`; the platform picks a shared host (or starts one), builds the repository
        (a Dockerfile at the root, or a generated one for Node, Python, Go and static projects), runs `instances` containers of `size`
        and serves them at `https://<name>.<apps domain>` with TLS. Name the repository with `repoUrl` (and `gitToken` for a private one)
        or with `installationId` and `repo` from the GitHub App, which also redeploys on every push. Poll until `live`; on `failed`, read the build log.
      parameters: [{ $ref: "#/components/parameters/idempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [name], properties: { name: { type: string, description: Hostname label, unique across the platform }, repoUrl: { type: string, format: uri }, installationId: { type: string }, repo: { type: string, example: acme/app }, gitToken: { type: string }, branch: { type: string, default: main }, port: { type: integer, default: 3000 }, size: { type: string, enum: [app-xs, app-s, app-m, app-l], default: app-xs }, instances: { type: integer, minimum: 1, maximum: 5, default: 1 }, env: { type: object, additionalProperties: { type: string } }, healthPath: { type: string }, region: { type: string }, project: { type: string } } } } } }
      responses: { "202": { description: Accepted, content: { application/json: { schema: { $ref: "#/components/schemas/PlatformApp" } } } }, "409": { description: Name taken (`name_taken`) } }
  /app-platform/apps/{id}:
    get: { tags: [app-platform], summary: Get an app, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/PlatformApp" } } } }, "404": { $ref: "#/components/responses/NotFound" } } }
    patch:
      tags: [app-platform]
      summary: Change configuration and deploy again
      description: "`env` replaces the whole set. A bigger size or more instances checks the spend limit and the host's room."
      parameters: [{ $ref: "#/components/parameters/id" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { branch: { type: string }, port: { type: integer }, size: { type: string, enum: [app-xs, app-s, app-m, app-l] }, instances: { type: integer, minimum: 1, maximum: 5 }, env: { type: object, additionalProperties: { type: string } }, healthPath: { type: string }, gitToken: { type: string } } } } } }
      responses: { "202": { description: Accepted, content: { application/json: { schema: { $ref: "#/components/schemas/PlatformApp" } } } } }
    delete: { tags: [app-platform], summary: Delete an app, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "202": { description: Accepted } } }
  /app-platform/apps/{id}/deploy:
    post: { tags: [app-platform], summary: Build and deploy the branch head now, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "202": { description: Accepted } } }
  /app-platform/apps/{id}/stop:
    post: { tags: [app-platform], summary: Stop the instances and the charge, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "200": { description: OK } } }
  /app-platform/apps/{id}/start:
    post: { tags: [app-platform], summary: Start a stopped app, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "200": { description: OK } } }
  /app-platform/apps/{id}/deploys:
    get: { tags: [app-platform], summary: Deploy history, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "200": { description: OK } } }
  /app-platform/apps/{id}/logs:
    get: { tags: [app-platform], summary: Build or runtime log, description: Fetched live from the host when reachable; the build log is cached on the app otherwise. Capped at 32 KB., parameters: [{ $ref: "#/components/parameters/id" }, { name: type, in: query, schema: { type: string, enum: [build, runtime], default: build } }], responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { id: { type: string }, type: { type: string }, log: { type: string }, live: { type: boolean }, updatedAt: { type: string, format: date-time } } } } } } } }
  /app-platform/apps/{id}/domains:
    post: { tags: [app-platform], summary: Attach a custom domain, description: Point a CNAME at the app hostname; the certificate is issued on the first request., parameters: [{ $ref: "#/components/parameters/id" }], requestBody: { required: true, content: { application/json: { schema: { type: object, required: [domain], properties: { domain: { type: string } } } } } }, responses: { "200": { description: OK }, "409": { description: Domain attached to another app (`domain_taken`) } } }
  /app-platform/apps/{id}/domains/{domain}:
    delete: { tags: [app-platform], summary: Detach a custom domain, parameters: [{ $ref: "#/components/parameters/id" }, { name: domain, in: path, required: true, schema: { type: string } }], responses: { "200": { description: OK } } }
  /kubernetes/versions:
    get: { tags: [kubernetes], summary: Kubernetes versions on offer, responses: { "200": { description: OK } } }
  /kubernetes/clusters:
    get: { tags: [kubernetes], summary: List clusters, parameters: [{ $ref: "#/components/parameters/project" }], responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/KubeCluster" } } } } } } } } }
    post:
      tags: [kubernetes]
      summary: Create a cluster
      description: Returns `202` with status `creating`. One control plane node is included; `ha` gives three behind one address for a flat monthly fee. Worker pools are sized like servers (at least 2 GB of memory) and billed as servers. Bootstrapping takes about ten minutes; poll until `active`, then fetch the kubeconfig.
      parameters: [{ $ref: "#/components/parameters/idempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [name, pools], properties: { name: { type: string }, version: { type: string, example: "1.31" }, region: { type: string }, project: { type: string }, ha: { type: boolean }, controlSize: { type: string }, pools: { type: array, minItems: 1, items: { $ref: "#/components/schemas/KubePoolInput" } } } } } } }
      responses: { "202": { description: Accepted, content: { application/json: { schema: { $ref: "#/components/schemas/KubeCluster" } } } }, "402": { description: Spend limit (`spend_limit_reached`) }, "429": { description: Quota (`quota_exceeded`) } }
  /kubernetes/clusters/{id}:
    get: { tags: [kubernetes], summary: Get a cluster with its pools, nodes and cloud resources, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/KubeCluster" } } } }, "404": { $ref: "#/components/responses/NotFound" } } }
    patch: { tags: [kubernetes], summary: Rename a cluster, parameters: [{ $ref: "#/components/parameters/id" }], requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { name: { type: string } } } } } }, responses: { "200": { description: OK } } }
    delete: { tags: [kubernetes], summary: Delete a cluster with its nodes and the load balancers and volumes it created, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "202": { description: Accepted } } }
  /kubernetes/clusters/{id}/kubeconfig:
    get: { tags: [kubernetes], summary: Admin kubeconfig as YAML, description: Needs `kubernetes:write`. Treat it like a password., parameters: [{ $ref: "#/components/parameters/id" }], responses: { "200": { description: OK, content: { application/yaml: { schema: { type: string } } } }, "409": { description: Cluster not bootstrapped yet (`invalid_state`) } } }
  /kubernetes/clusters/{id}/pools:
    post: { tags: [kubernetes], summary: Add a worker pool, parameters: [{ $ref: "#/components/parameters/id" }], requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/KubePoolInput" } } } }, responses: { "202": { description: Accepted, content: { application/json: { schema: { $ref: "#/components/schemas/KubeCluster" } } } } } }
  /kubernetes/clusters/{id}/pools/{poolId}:
    patch: { tags: [kubernetes], summary: Scale a pool, description: Growing adds servers; shrinking drains and removes the highest numbered nodes first., parameters: [{ $ref: "#/components/parameters/id" }, { name: poolId, in: path, required: true, schema: { type: string } }], requestBody: { required: true, content: { application/json: { schema: { type: object, required: [count], properties: { count: { type: integer, minimum: 0, maximum: 50 } } } } } }, responses: { "202": { description: Accepted } } }
    delete: { tags: [kubernetes], summary: Remove a pool and its nodes, parameters: [{ $ref: "#/components/parameters/id" }, { name: poolId, in: path, required: true, schema: { type: string } }], responses: { "202": { description: Accepted }, "400": { description: The last pool cannot be removed } } }
  /support/plans:
    get:
      tags: [support]
      summary: Support plan catalog
      description: Public. Plans with first response targets per priority (hours, null when the priority is not allowed on the plan) and the monthly price in the requested currency.
      security: []
      parameters: [{ name: currency, in: query, schema: { type: string, enum: [USD, SAR], default: USD } }]
      responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/SupportPlan" } } } } } } } }
  /support/plan:
    get:
      tags: [support]
      summary: The team's support plan
      responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/SupportPlanStatus" } } } } }
    put:
      tags: [support]
      summary: Change the support plan
      description: Needs `billing:write`. An upgrade checks the spend limit on the team's oldest project; a downgrade applies at once. Open tickets keep the targets of the plan they were opened under.
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [plan], properties: { plan: { type: string, enum: [free, developer, standard, premium] } } } } } }
      responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/SupportPlanStatus" } } } } }
  /support/tickets:
    get:
      tags: [support]
      summary: List tickets
      parameters: [{ name: status, in: query, schema: { type: string, enum: [open, answered, closed, all], default: all } }, { $ref: "#/components/parameters/limit" }, { $ref: "#/components/parameters/cursor" }]
      responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/Ticket" } }, meta: { $ref: "#/components/schemas/PageMeta" } } } } } } }
    post:
      tags: [support]
      summary: Open a ticket
      description: Priority must be allowed on the team's plan (`400` with the allowed list otherwise). `resource` names what the ticket is about and must belong to the team. Owners and the opener get every answer by email.
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [subject, body], properties: { subject: { type: string, minLength: 3, maxLength: 140 }, body: { type: string }, priority: { type: string, enum: [low, normal, high, urgent], default: normal }, resource: { type: string, description: '"server:<id>", "database:<id>", "load_balancer:<id>", "volume:<id>", "domain:<id>", "bucket:<id>" or "invoice:<id>"' } } } } } }
      responses: { "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/Ticket" } } } }, "429": { description: Open ticket limit for the plan (`quota_exceeded`) } }
  /support/tickets/{id}:
    get:
      tags: [support]
      summary: Get a ticket with its messages
      parameters: [{ $ref: "#/components/parameters/id" }]
      responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Ticket" } } } }, "404": { $ref: "#/components/responses/NotFound" } }
  /support/tickets/{id}/messages:
    post:
      tags: [support]
      summary: Reply on a ticket
      description: Reopens a closed ticket for up to 14 days after it was closed.
      parameters: [{ $ref: "#/components/parameters/id" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [body], properties: { body: { type: string } } } } } }
      responses: { "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/Ticket" } } } } }
  /support/tickets/{id}/close:
    post:
      tags: [support]
      summary: Close a ticket
      parameters: [{ $ref: "#/components/parameters/id" }]
      responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Ticket" } } } } }
  /managed/plans:
    get:
      tags: [managed-cloud]
      summary: Active managed cloud plans
      description: Prices exclude VAT. `priceMinor` is null for a custom plan. Targets are minutes per priority; on BUSINESS_HOURS plans they count working minutes (09:00 to 17:00 on working days of the contract calendar, public holidays excluded).
      responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/ManagedCloudPlan" } } } } } } } }
  /managed/contracts:
    get:
      tags: [managed-cloud]
      summary: The team's contracts
      description: Team owners only.
      responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/ManagedCloudContract" } } } } } } }, "403": { $ref: "#/components/responses/Forbidden" } }
    post:
      tags: [managed-cloud]
      summary: Request a managed cloud plan
      description: Team owners only. Creates a DRAFT contract with the default responsibility matrix. A support lead reviews it, signs it and starts onboarding; the contract becomes ACTIVE, and billing starts, when onboarding is complete. One pending request per team.
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [plan], properties: { plan: { type: string, example: ESSENTIAL }, calendar: { type: string, enum: [SA, TR], description: Business hours calendar; defaults from the team country }, notes: { type: string, maxLength: 4000 } } } } } }
      responses: { "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/ManagedCloudContract" } } } }, "409": { description: A request is already pending (`request_pending`) }, "422": { $ref: "#/components/responses/Invalid" } }
  /managed/contracts/{id}:
    get:
      tags: [managed-cloud]
      summary: Contract detail with SLA, responsibility matrix, onboarding progress and this month's engineer time
      description: Team owners only.
      parameters: [{ $ref: "#/components/parameters/id" }]
      responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ManagedCloudContract" } } } }, "404": { $ref: "#/components/responses/NotFound" } }
  /managed/contracts/{id}/assets:
    get:
      tags: [managed-cloud]
      summary: Assets under management with health
      parameters: [{ $ref: "#/components/parameters/id" }]
      responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/ManagedCloudAsset" } } } } } } }, "404": { $ref: "#/components/responses/NotFound" } }
    post:
      tags: [managed-cloud]
      summary: Ask for an asset to be managed
      description: Team owners only. The asset stays PENDING until an engineer approves it. PLATFORM_SERVER needs `serverId`; EXTERNAL_SERVER and SITE need `address`. Limited by the plan's `maxAssets`.
      parameters: [{ $ref: "#/components/parameters/id" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [kind, name], properties: { kind: { type: string, enum: [PLATFORM_SERVER, EXTERNAL_SERVER, SITE] }, name: { type: string }, serverId: { type: string }, address: { type: string, description: "Public IP, hostname or site URL" }, provider: { type: string }, os: { type: string }, notes: { type: string } } } } } }
      responses: { "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/ManagedCloudAsset" } } } }, "403": { description: "Not an owner, or the plan's asset limit is reached (`quota_exceeded`)" } }
  /managed/assets:
    get:
      tags: [managed-cloud]
      summary: Every managed asset of the team
      responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/ManagedCloudAsset" } } } } } } } }
  /managed/contracts/{id}/reports:
    get:
      tags: [managed-cloud]
      summary: Monthly reports that were sent
      description: Team owners only. Reports are drafted on the 1st and sent by the 3rd.
      parameters: [{ $ref: "#/components/parameters/id" }]
      responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/ManagedCloudMonthlyReport" } } } } } } } }
  /managed/contracts/{id}/reports/{reportId}/pdf:
    get:
      tags: [managed-cloud]
      summary: Download a monthly report as PDF
      parameters: [{ $ref: "#/components/parameters/id" }, { name: reportId, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: OK, content: { application/pdf: { schema: { type: string, format: binary } } } }, "404": { $ref: "#/components/responses/NotFound" } }
  /managed/tickets:
    get:
      tags: [managed-cloud]
      summary: Managed cloud tickets
      parameters: [{ name: status, in: query, schema: { type: string, enum: [open, answered, closed, all], default: all } }, { name: priority, in: query, schema: { $ref: "#/components/schemas/ManagedPriority" } }, { name: contractId, in: query, schema: { type: string } }, { $ref: "#/components/parameters/limit" }, { $ref: "#/components/parameters/cursor" }]
      responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/ManagedCloudTicket" } }, meta: { $ref: "#/components/schemas/PageMeta" } } } } } } }
    post:
      tags: [managed-cloud]
      summary: Open a ticket
      description: Due times come from the plan's targets and the contract calendar. P1 and P2 page the engineer on call. `contractId` is needed only when the team has more than one contract; `assetId` implies it. Not allowed while the contract is suspended.
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [subject, body], properties: { subject: { type: string, minLength: 3, maxLength: 140 }, body: { type: string }, priority: { allOf: [{ $ref: "#/components/schemas/ManagedPriority" }], default: P3 }, contractId: { type: string }, assetId: { type: string } } } } } }
      responses: { "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/ManagedCloudTicket" } } } }, "409": { description: The contract is not onboarding or active (`invalid_state`) } }
  /managed/tickets/{id}:
    get:
      tags: [managed-cloud]
      summary: A ticket with its messages
      description: Internal engineer notes are never included.
      parameters: [{ $ref: "#/components/parameters/id" }]
      responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ManagedCloudTicket" } } } }, "404": { $ref: "#/components/responses/NotFound" } }
  /managed/tickets/{id}/messages:
    post:
      tags: [managed-cloud]
      summary: Reply on a ticket
      description: Reopens a closed ticket for up to 14 days after it was closed.
      parameters: [{ $ref: "#/components/parameters/id" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [body], properties: { body: { type: string } } } } } }
      responses: { "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/ManagedCloudTicket" } } } } }
  /managed/tickets/{id}/close:
    post:
      tags: [managed-cloud]
      summary: Close a ticket
      parameters: [{ $ref: "#/components/parameters/id" }]
      responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/ManagedCloudTicket" } } } } }
  /databases:
    get: { tags: [databases], summary: List managed database clusters, parameters: [{ $ref: "#/components/parameters/project" }], responses: { "200": { description: OK, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/DatabaseCluster" } } } } } } } } }
    post:
      tags: [databases]
      summary: Create a cluster
      description: One node, or three nodes with automatic failover. Returns 202; the cluster becomes `active` in a few minutes. Priced per node per month.
      parameters: [{ $ref: "#/components/parameters/idempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [name, engine, size], properties: { name: { type: string }, engine: { type: string, enum: [postgres, valkey, mysql] }, version: { type: string }, size: { type: string, description: A server size id with at least 1 GB of memory }, nodes: { type: integer, enum: [1, 3], default: 1 }, region: { type: string }, project: { type: string }, trustedSources: { type: array, items: { type: string } }, backupHourUtc: { type: integer, minimum: 0, maximum: 23, default: 2 } } } } } }
      responses: { "202": { description: Accepted, content: { application/json: { schema: { $ref: "#/components/schemas/DatabaseCluster" } } } } }
  /databases/engines:
    get: { tags: [databases], summary: Engines and versions on offer, responses: { "200": { description: OK } } }
  /databases/{id}:
    get: { tags: [databases], summary: Get a cluster with connection details and secrets, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/DatabaseCluster" } } } }, "404": { $ref: "#/components/responses/NotFound" } } }
    patch: { tags: [databases], summary: Change trusted sources or the backup hour, parameters: [{ $ref: "#/components/parameters/id" }], requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { trustedSources: { type: array, items: { type: string } }, backupHourUtc: { type: integer } } } } } }, responses: { "202": { description: Accepted } } }
    delete: { tags: [databases], summary: Delete a cluster and its nodes, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "202": { description: Accepted } } }
  /databases/{id}/users:
    post: { tags: [databases], summary: Add a user (password returned once), parameters: [{ $ref: "#/components/parameters/id" }], requestBody: { required: true, content: { application/json: { schema: { type: object, required: [name], properties: { name: { type: string } } } } } }, responses: { "201": { description: Created } } }
  /databases/{id}/users/{userId}:
    delete: { tags: [databases], summary: Delete a user, parameters: [{ $ref: "#/components/parameters/id" }, { name: userId, in: path, required: true, schema: { type: string } }], responses: { "200": { description: Deleted } } }
  /databases/{id}/users/{userId}/reset-password:
    post: { tags: [databases], summary: Reset a user's password (returned once), parameters: [{ $ref: "#/components/parameters/id" }, { name: userId, in: path, required: true, schema: { type: string } }], responses: { "200": { description: OK } } }
  /databases/{id}/dbs:
    post: { tags: [databases], summary: Add a database, parameters: [{ $ref: "#/components/parameters/id" }], requestBody: { required: true, content: { application/json: { schema: { type: object, required: [name], properties: { name: { type: string } } } } } }, responses: { "201": { description: Created } } }
  /databases/{id}/dbs/{dbId}:
    delete: { tags: [databases], summary: Remove a database from the cluster, parameters: [{ $ref: "#/components/parameters/id" }, { name: dbId, in: path, required: true, schema: { type: string } }], responses: { "200": { description: Deleted } } }
  /databases/{id}/backups:
    get: { tags: [databases], summary: List backups, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "200": { description: OK } } }
    post: { tags: [databases], summary: Take a backup now, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "202": { description: Accepted } } }

  /apps:
    get: { tags: [marketplace], security: [], summary: List marketplace apps, parameters: [{ name: category, in: query, schema: { type: string } }], responses: { "200": { description: OK } } }
  /apps/categories:
    get: { tags: [marketplace], security: [], summary: App categories with counts, responses: { "200": { description: OK } } }
  /apps/{slug}:
    get:
      tags: [marketplace]
      security: []
      summary: Get an app (its `variables` describe the one-click form)
      parameters: [{ name: slug, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: OK }, "404": { $ref: "#/components/responses/NotFound" } }

  /billing/balance:
    get: { tags: [billing], summary: Credit balance, month-to-date spend and account status, responses: { "200": { description: OK } } }
  /billing/usage:
    get:
      tags: [billing]
      summary: Rated usage per resource
      parameters: [{ $ref: "#/components/parameters/project" }, { name: from, in: query, schema: { type: string, format: date-time } }, { name: to, in: query, schema: { type: string, format: date-time } }]
      responses: { "200": { description: OK } }
  /billing/invoices:
    get: { tags: [billing], summary: List invoices, responses: { "200": { description: OK } } }
  /billing/invoices/{id}:
    get: { tags: [billing], summary: Get an invoice with its usage lines, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "200": { description: OK } } }

  /webhooks:
    get: { tags: [webhooks], summary: List webhooks, responses: { "200": { description: OK } } }
    post:
      tags: [webhooks]
      summary: Create a webhook
      description: "Deliveries are signed with `X-Prgd-Signature: sha256=<hmac>` using the secret returned once at creation."
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [url, events], properties: { url: { type: string, format: uri }, events: { type: array, items: { $ref: "#/components/schemas/EventName" } } } } } } }
      responses: { "201": { description: Created } }
  /webhooks/events:
    get: { tags: [webhooks], summary: List subscribable event names, responses: { "200": { description: OK } } }
  /webhooks/{id}:
    delete: { tags: [webhooks], summary: Delete a webhook, parameters: [{ $ref: "#/components/parameters/id" }], responses: { "204": { description: Deleted } } }

components:
  securitySchemes:
    bearerAuth: { type: http, scheme: bearer }
  parameters:
    id: { name: id, in: path, required: true, schema: { type: string } }
    project: { name: project, in: query, description: Project id or slug (default `default`), schema: { type: string } }
    limit: { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
    cursor: { name: cursor, in: query, schema: { type: string } }
    idempotencyKey: { name: Idempotency-Key, in: header, schema: { type: string, format: uuid } }
  responses:
    NotFound: { description: Not found, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    Unauthorized: { description: Unauthorized, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    Forbidden: { description: Forbidden, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    Conflict: { description: Conflict, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    Invalid: { description: Validation error, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [invalid_request, unauthorized, forbidden, approval_required, not_found, conflict, invalid_state, quota_exceeded, spend_limit_reached, verification_required, account_suspended, idempotency_key_reused, rate_limited, workflow_unavailable, internal_error]
            message: { type: string }
            details: { type: object, additionalProperties: true }
    PageMeta:
      type: object
      properties: { next_cursor: { type: [string, "null"] }, count: { type: integer } }
    Approval:
      type: object
      description: A request from an agent token that a person must approve before it runs.
      properties:
        id: { type: string }
        kind: { type: string, example: "servers:delete" }
        summary: { type: string, example: "Delete server web-1 (s-2vcpu-4gb)" }
        status: { type: string, enum: [pending, approved, denied, expired, failed] }
        resourceType: { type: string }
        resourceId: { type: string, nullable: true }
        resourceName: { type: string, nullable: true }
        payload: { type: object, description: The original request body }
        result: { type: object, nullable: true, description: What the action returned once approved }
        reason: { type: string, nullable: true }
        token: { type: object, nullable: true, properties: { name: { type: string }, prefix: { type: string } } }
        decidedBy: { type: object, nullable: true, properties: { name: { type: string }, email: { type: string } } }
        decidedAt: { type: string, format: date-time, nullable: true }
        expiresAt: { type: string, format: date-time }
        createdAt: { type: string, format: date-time }
    Checkout:
      type: object
      properties:
        paymentId: { type: string }
        provider: { type: string, enum: [moyasar, fake] }
        amountMinor: { type: integer }
        currency: { type: string, enum: [USD, SAR] }
        redirectUrl: { type: string, description: Hosted payment page }
    MetricPoint:
      type: object
      properties:
        at: { type: string, format: date-time }
        cpu: { type: number, description: percent }
        cpuMax: { type: number, description: peak within the hour (hourly resolution only) }
        memoryUsedMb: { type: integer }
        memoryTotalMb: { type: integer }
        netInBps: { type: number }
        netOutBps: { type: number }
        diskReadBps: { type: number }
        diskWriteBps: { type: number }
        diskUsedPercent: { type: number, nullable: true }
    MetricSeries:
      type: object
      properties:
        serverId: { type: string }
        period: { type: string }
        resolution: { type: string, enum: [minute, hour] }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        latest: { $ref: "#/components/schemas/MetricPoint" }
        points: { type: array, items: { $ref: "#/components/schemas/MetricPoint" } }
    AlertRuleInput:
      type: object
      properties:
        name: { type: string }
        metric: { type: string, enum: [cpu, memory, disk, net_in, net_out] }
        comparator: { type: string, enum: [above, below], default: above }
        threshold: { type: number, description: percent for cpu, memory and disk; Mbps for network }
        windowMinutes: { type: integer, default: 5 }
        serverIds: { type: array, items: { type: string } }
        tags: { type: array, items: { type: string } }
        emails: { type: array, items: { type: string, format: email } }
        enabled: { type: boolean }
    AlertRule:
      allOf:
        - $ref: "#/components/schemas/AlertRuleInput"
        - type: object
          properties:
            id: { type: string }
            createdAt: { type: string, format: date-time }
    TotpCode:
      type: object
      required: [code]
      properties:
        code: { type: string, description: Six digit authenticator code, or a recovery code }
    Signup:
      type: object
      required: [email, password, name, teamName]
      properties:
        email: { type: string, format: email }
        password: { type: string, minLength: 10 }
        name: { type: string }
        teamName: { type: string }
        country: { type: string, default: SA }
        locale: { type: string, enum: [en, tr, ar] }
    Session:
      type: object
      properties:
        user: { type: object }
        team: { type: object }
        session: { type: string, description: JWT to use as Bearer token }
    CreateToken:
      type: object
      required: [name, scopes]
      properties:
        name: { type: string }
        scopes: { type: array, items: { $ref: "#/components/schemas/Scope" } }
        projectId: { type: string }
        isAgent: { type: boolean }
        spendCapMinor: { type: integer }
        requireApprovalFor: { type: array, items: { type: string }, example: ["servers:delete", "servers:resize-down"] }
        expiresInDays: { type: integer }
    Scope:
      type: string
      enum: [servers:read, servers:write, servers:delete, images:read, snapshots:read, snapshots:write, volumes:read, volumes:write, dns:read, dns:write, storage:read, storage:write, databases:read, databases:write, network:read, network:write, apps:read, billing:read, billing:write, managed:read, managed:write, iam:read, iam:write]
    Size:
      type: object
      properties:
        id: { type: string, example: s-2vcpu-4gb }
        vcpu: { type: integer }
        memoryMb: { type: integer }
        diskGb: { type: integer }
        transferTb: { type: number }
        family: { type: string, enum: [shared, dedicated, gpu] }
    ServerStatus:
      type: string
      enum: [new, provisioning, active, off, rebooting, resizing, rebuilding, deleting, deleted, failed, suspended]
    CreateServer:
      type: object
      required: [name, size, image]
      properties:
        name: { type: string, pattern: "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$" }
        size: { type: string, example: s-2vcpu-4gb }
        image: { type: string, description: Image id or marketplace app slug, example: ubuntu-24-04 }
        region: { type: string, default: sa1 }
        project: { type: string, default: default }
        sshKeys: { type: array, items: { type: string } }
        userData: { type: string, description: cloud-init user-data }
        tags: { type: array, items: { type: string } }
        backups: { type: boolean }
        managed: { type: boolean, description: Managed tier, daily backups included }
        firewalls: { type: array, items: { type: string } }
        appVariables: { type: object, additionalProperties: { type: string }, description: Marketplace app variables }
        avoid: { type: array, items: { type: string }, description: Server ids to anti-affine from }
    Volume:
      type: object
      required: [id, name, sizeGb, status, regionId, projectId, createdAt]
      properties:
        id: { type: string }
        name: { type: string }
        sizeGb: { type: integer }
        status: { type: string, enum: [creating, available, attaching, attached, detaching, resizing, deleting, failed, deleted] }
        statusMessage: { type: string, nullable: true, description: Why the last transition failed, if it did }
        serverId: { type: string, nullable: true }
        device: { type: string, nullable: true, description: Guest path, /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_<serial> }
        regionId: { type: string }
        projectId: { type: string }
        createdAt: { type: string, format: date-time }
        server: { type: object, nullable: true, properties: { id: { type: string }, name: { type: string } } }
    CreateVolume:
      type: object
      required: [name, sizeGb]
      properties:
        name: { type: string, pattern: "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", maxLength: 64 }
        sizeGb: { type: integer, minimum: 10, maximum: 16384 }
        region: { type: string, description: Defaults to the platform region }
        project: { type: string }
        serverId: { type: string, description: Attach right after creation }
    DatabaseCluster:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        engine: { type: string, enum: [postgres, valkey, mysql] }
        version: { type: string }
        status: { type: string, enum: [creating, active, updating, failed, deleting, deleted] }
        statusMessage: { type: string, nullable: true }
        nodes: { type: integer }
        size: { type: object, properties: { id: { type: string }, vcpu: { type: integer }, memoryMb: { type: integer }, diskGb: { type: integer } } }
        port: { type: integer }
        poolerPort: { type: integer, nullable: true }
        trustedSources: { type: array, items: { type: string } }
        backupHourUtc: { type: integer }
        connection: { type: object, description: Secrets only on GET by id, properties: { host: { type: string, nullable: true }, privateHost: { type: string, nullable: true }, port: { type: integer }, database: { type: string }, user: { type: string }, password: { type: string }, ssl: { type: boolean }, uri: { type: string, nullable: true }, privateUri: { type: string, nullable: true }, appUri: { type: string, nullable: true } } }
        users: { type: array, items: { type: object, properties: { id: { type: string }, name: { type: string }, password: { type: string } } } }
        databases: { type: array, items: { type: object, properties: { id: { type: string }, name: { type: string } } } }
        nodeStatus: { type: array, items: { type: object, properties: { index: { type: integer }, status: { type: string }, role: { type: string }, appliedVersion: { type: integer }, lagBytes: { type: integer, nullable: true }, lastSeenAt: { type: string, nullable: true } } } }
        createdAt: { type: string, format: date-time }
    Bucket:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        status: { type: string, enum: [creating, active, deleting, deleted, failed] }
        statusMessage: { type: string, nullable: true }
        regionId: { type: string }
        projectId: { type: string }
        public: { type: boolean }
        sizeBytes: { type: integer }
        objectCount: { type: integer }
        usageUpdatedAt: { type: string, format: date-time, nullable: true }
        endpoint: { type: string, description: S3 endpoint for any client }
        url: { type: string, description: Path style URL of the bucket }
        createdAt: { type: string, format: date-time }
    Domain:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        status: { type: string, enum: [pending, active, error, deleting, deleted] }
        statusMessage: { type: string, nullable: true }
        serial: { type: integer }
        synced: { type: boolean, description: True when the nameservers serve the current serial }
        nameservers: { type: array, items: { type: string } }
        recordCount: { type: integer }
        records: { type: array, items: { $ref: "#/components/schemas/DnsRecord" } }
        createdAt: { type: string, format: date-time }
    DnsRecord:
      type: object
      properties:
        id: { type: string }
        name: { type: string, description: Relative to the zone; "@" is the apex }
        type: { type: string, enum: [A, AAAA, CNAME, MX, TXT, NS, SRV, CAA] }
        content: { type: string }
        ttl: { type: integer }
        priority: { type: integer, nullable: true, description: MX preference or SRV priority }
        updatedAt: { type: string, format: date-time }
    CreateDnsRecord:
      type: object
      required: [name, type, content]
      properties:
        name: { type: string, example: www }
        type: { type: string, enum: [A, AAAA, CNAME, MX, TXT, NS, SRV, CAA] }
        content: { type: string, description: "A: IPv4; CNAME/MX/NS: hostname; SRV: weight port target; CAA: flags tag value; TXT: text" }
        ttl: { type: integer, minimum: 30, maximum: 604800, default: 3600 }
        priority: { type: integer, description: MX and SRV }
    ForwardingRule:
      type: object
      required: [entryProtocol, entryPort, targetProtocol, targetPort]
      properties:
        entryProtocol: { type: string, enum: [http, https, tcp] }
        entryPort: { type: integer }
        targetProtocol: { type: string, enum: [http, tcp] }
        targetPort: { type: integer }
        certificateId: { type: string, description: Required for https }
    HealthCheck:
      type: object
      properties:
        protocol: { type: string, enum: [http, tcp] }
        port: { type: integer }
        path: { type: string }
        intervalSeconds: { type: integer }
        timeoutSeconds: { type: integer }
        healthyThreshold: { type: integer }
        unhealthyThreshold: { type: integer }
    LoadBalancer:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        status: { type: string, enum: [creating, active, updating, failed, deleting, deleted] }
        statusMessage: { type: string, nullable: true }
        ip: { type: string, nullable: true, description: The load balancer's public IP (VIP) }
        regionId: { type: string }
        projectId: { type: string }
        algorithm: { type: string, enum: [round_robin, least_conn] }
        nodes: { type: integer, description: HAProxy nodes sharing the IP }
        forwardingRules: { type: array, items: { $ref: "#/components/schemas/ForwardingRule" } }
        healthCheck: { $ref: "#/components/schemas/HealthCheck" }
        stickySessions: { type: object, nullable: true, properties: { type: { type: string, enum: [cookie] }, cookieName: { type: string }, ttlSeconds: { type: integer } } }
        redirectHttpToHttps: { type: boolean }
        proxyProtocol: { type: boolean }
        tag: { type: string, nullable: true, description: Servers with this tag are targets automatically }
        configVersion: { type: integer }
        nodeStatus: { type: array, items: { type: object, properties: { index: { type: integer }, status: { type: string }, appliedVersion: { type: integer }, lastSeenAt: { type: string, format: date-time, nullable: true } } } }
        targets: { type: array, items: { type: object, properties: { serverId: { type: string }, name: { type: string }, status: { type: string }, healthy: { type: boolean, nullable: true }, lastCheckedAt: { type: string, format: date-time, nullable: true } } } }
        createdAt: { type: string, format: date-time }
    CreateLoadBalancer:
      type: object
      required: [name, forwardingRules]
      properties:
        name: { type: string, pattern: "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", maxLength: 64 }
        region: { type: string }
        project: { type: string }
        nodes: { type: integer, minimum: 1, maximum: 3, default: 1 }
        algorithm: { type: string, enum: [round_robin, least_conn] }
        forwardingRules: { type: array, items: { $ref: "#/components/schemas/ForwardingRule" } }
        healthCheck: { $ref: "#/components/schemas/HealthCheck" }
        stickySessions: { type: object, properties: { type: { type: string, enum: [none, cookie] }, cookieName: { type: string }, ttlSeconds: { type: integer } } }
        redirectHttpToHttps: { type: boolean }
        proxyProtocol: { type: boolean }
        serverIds: { type: array, items: { type: string } }
        tag: { type: string }
    UpdateLoadBalancer:
      type: object
      properties:
        name: { type: string }
        algorithm: { type: string, enum: [round_robin, least_conn] }
        forwardingRules: { type: array, items: { $ref: "#/components/schemas/ForwardingRule" } }
        healthCheck: { $ref: "#/components/schemas/HealthCheck" }
        stickySessions: { type: object, properties: { type: { type: string, enum: [none, cookie] }, cookieName: { type: string }, ttlSeconds: { type: integer } } }
        redirectHttpToHttps: { type: boolean }
        proxyProtocol: { type: boolean }
        tag: { type: string }
    Server:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        status: { $ref: "#/components/schemas/ServerStatus" }
        statusMessage: { type: [string, "null"] }
        region: { type: object, properties: { id: { type: string }, name: { type: string } } }
        size: { $ref: "#/components/schemas/Size" }
        image: { type: object }
        networks:
          type: object
          properties:
            v4: { type: array, items: { type: object, properties: { ipAddress: { type: string }, type: { type: string }, floating: { type: boolean } } } }
            private: { type: array, items: { type: object, properties: { ipAddress: { type: string } } } }
        firewalls: { type: array, items: { type: string } }
        tags: { type: array, items: { type: string } }
        backupsEnabled: { type: boolean }
        managed: { type: boolean }
        managedHealth: { type: [string, "null"], enum: [ok, warn, stale, pending, null] }
        projectId: { type: string }
        createdAt: { type: string, format: date-time }
    PlatformApp:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        status: { type: string, enum: [creating, building, live, failed, stopped, deleting, deleted] }
        statusMessage: { type: [string, "null"] }
        url: { type: string }
        hostname: { type: string }
        customDomains: { type: array, items: { type: string } }
        region: { type: object, properties: { id: { type: string }, name: { type: string } } }
        repoUrl: { type: string }
        repo: { type: [string, "null"] }
        source: { type: string, enum: [github_app, url] }
        branch: { type: string }
        port: { type: integer }
        size: { type: object, properties: { id: { type: string }, memoryMb: { type: integer }, cpus: { type: number } } }
        instances: { type: integer }
        healthPath: { type: [string, "null"] }
        env: { type: object, additionalProperties: { type: string } }
        hostIp: { type: [string, "null"] }
        lastCommit: { type: [string, "null"] }
        lastDeployAt: { type: [string, "null"], format: date-time }
        deploys: { type: array, items: { type: object, properties: { id: { type: string }, status: { type: string, enum: [queued, building, live, failed] }, trigger: { type: string }, commit: { type: [string, "null"] }, startedAt: { type: string, format: date-time }, finishedAt: { type: [string, "null"], format: date-time } } } }
        projectId: { type: string }
        createdAt: { type: string, format: date-time }
    KubePoolInput:
      type: object
      required: [name, size, count]
      properties:
        name: { type: string }
        size: { type: string, description: Server size id with at least 2 GB of memory }
        count: { type: integer, minimum: 1, maximum: 50 }
        labels: { type: object, additionalProperties: { type: string } }
        taints: { type: array, items: { type: object, properties: { key: { type: string }, value: { type: string }, effect: { type: string, enum: [NoSchedule, PreferNoSchedule, NoExecute] } } } }
    KubeNode:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        role: { type: string, enum: [control, worker] }
        index: { type: integer }
        poolId: { type: [string, "null"] }
        status: { type: string, description: Server status }
        ready: { type: boolean }
        kubeVersion: { type: [string, "null"] }
        ip: { type: [string, "null"] }
        privateIp: { type: [string, "null"] }
        lastSeenAt: { type: [string, "null"], format: date-time }
    KubeCluster:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        version: { type: string }
        status: { type: string, enum: [creating, active, updating, failed, deleting, deleted] }
        statusMessage: { type: [string, "null"] }
        ha: { type: boolean }
        region: { type: object, properties: { id: { type: string }, name: { type: string } } }
        controlSize: { type: object, properties: { id: { type: string }, vcpu: { type: integer }, memoryMb: { type: integer }, diskGb: { type: integer } } }
        endpoint: { type: [string, "null"], description: "https://<address>:6443" }
        host: { type: [string, "null"] }
        podCidr: { type: string }
        serviceCidr: { type: string }
        configVersion: { type: integer }
        pools: { type: array, items: { type: object, properties: { id: { type: string }, name: { type: string }, size: { type: object }, count: { type: integer }, labels: { type: object }, taints: { type: array, items: { type: object } }, nodes: { type: array, items: { $ref: "#/components/schemas/KubeNode" } } } } }
        controlPlane: { type: array, items: { $ref: "#/components/schemas/KubeNode" } }
        cloud:
          type: object
          description: What the cloud controller created for the cluster
          properties:
            loadBalancers: { type: array, items: { type: object, properties: { service: { type: string, description: "namespace/name" }, loadBalancerId: { type: string }, ip: { type: [string, "null"] } } } }
            volumes: { type: array, items: { type: object, properties: { claim: { type: string, description: "namespace/name" }, volumeId: { type: string }, node: { type: string }, sizeGb: { type: integer }, mounted: { type: boolean } } } }
        workers: { type: integer }
        readyNodes: { type: integer }
        projectId: { type: string }
        createdAt: { type: string, format: date-time }
    SupportPlan:
      type: object
      properties:
        id: { type: string, enum: [free, developer, standard, premium] }
        name: { type: string }
        summary: { type: string }
        features: { type: array, items: { type: string } }
        targets: { type: object, description: First response target in hours per priority; null when not allowed, properties: { low: { type: [number, "null"] }, normal: { type: [number, "null"] }, high: { type: [number, "null"] }, urgent: { type: [number, "null"] } } }
        maxOpen: { type: integer }
        currency: { type: string }
        monthlyMinor: { type: integer }
    SupportPlanStatus:
      type: object
      properties:
        plan: { type: string, enum: [free, developer, standard, premium] }
        since: { type: [string, "null"], format: date-time }
        openTickets: { type: integer }
        details: { $ref: "#/components/schemas/SupportPlan" }
    Ticket:
      type: object
      properties:
        id: { type: string }
        number: { type: integer }
        subject: { type: string }
        status: { type: string, enum: [open, answered, closed], description: open waits on us, answered waits on you }
        priority: { type: string, enum: [low, normal, high, urgent] }
        plan: { type: string, description: Plan the ticket was opened under }
        resource: { type: [string, "null"] }
        firstResponseDueAt: { type: [string, "null"], format: date-time }
        firstRespondedAt: { type: [string, "null"], format: date-time }
        lastCustomerAt: { type: string, format: date-time }
        lastSupportAt: { type: [string, "null"], format: date-time }
        closedAt: { type: [string, "null"], format: date-time }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        messageCount: { type: integer }
        messages: { type: array, description: Present on a single ticket, items: { type: object, properties: { id: { type: string }, fromSupport: { type: boolean }, author: { type: string }, body: { type: string }, createdAt: { type: string, format: date-time } } } }
    ManagedReport:
      type: object
      properties:
        agentVersion: { type: integer }
        hostname: { type: string }
        kernel: { type: string }
        uptimeSec: { type: integer }
        load1: { type: number }
        memTotalMb: { type: integer }
        memUsedMb: { type: integer }
        diskTotalGb: { type: number }
        diskUsedGb: { type: number }
        diskUsedPct: { type: integer }
        pendingUpdates: { type: integer }
        securityUpdates: { type: integer }
        rebootRequired: { type: boolean }
        lastUpgradeAt: { type: [string, "null"], format: date-time }
        failedUnits: { type: array, items: { type: string } }
        sshBanned: { type: integer }
        sshPasswordAuth: { type: boolean }
    ManagedStatus:
      type: object
      properties:
        serverId: { type: string }
        managed: { type: boolean }
        backupsEnabled: { type: boolean }
        health: { type: [string, "null"], enum: [ok, warn, stale, pending, null] }
        issues: { type: array, items: { type: string } }
        reportedAt: { type: [string, "null"], format: date-time }
        report: { oneOf: [{ $ref: "#/components/schemas/ManagedReport" }, { type: "null" }] }
        installCommand: { type: [string, "null"], description: Present while the agent is not reporting }
    ManagedPriority:
      type: string
      enum: [P1, P2, P3, P4]
      description: P1 production down, P2 degraded service, P3 normal request, P4 question or low impact.
    ManagedTargets:
      type: object
      description: Minutes per priority.
      properties: { P1: { type: number }, P2: { type: number }, P3: { type: number }, P4: { type: number } }
    ManagedCloudPlan:
      type: object
      properties:
        id: { type: string }
        code: { type: string, example: ESSENTIAL }
        name: { type: string }
        description: { type: [string, "null"] }
        priceMinor: { type: [integer, "null"], description: "Monthly fee excluding VAT; null for a custom price" }
        currency: { type: string, enum: [SAR, USD] }
        custom: { type: boolean }
        maxAssets: { type: [integer, "null"] }
        coverage: { type: string, enum: [BUSINESS_HOURS, TWENTY_FOUR_SEVEN] }
        includedEngineerMinutes: { type: integer }
        hourlyRateMinor: { type: integer, description: "Engineer time beyond the included minutes, per hour" }
        responseTargets: { $ref: "#/components/schemas/ManagedTargets" }
        resolveTargets: { $ref: "#/components/schemas/ManagedTargets" }
        active: { type: boolean }
    ManagedCloudContract:
      type: object
      properties:
        id: { type: string }
        teamId: { type: string }
        status: { type: string, enum: [DRAFT, ONBOARDING, ACTIVE, SUSPENDED, CANCELLED] }
        plan: { $ref: "#/components/schemas/ManagedCloudPlan" }
        calendar: { type: string, enum: [SA, TR] }
        currency: { type: string, enum: [SAR, USD] }
        monthlyFeeMinor: { type: [integer, "null"], description: "In the contract currency, excluding VAT" }
        hourlyRateMinor: { type: integer }
        includedEngineerMinutes: { type: integer }
        maxAssets: { type: [integer, "null"] }
        liabilityCapMinor: { type: [integer, "null"] }
        signedByName: { type: [string, "null"] }
        signedAt: { type: [string, "null"], format: date-time }
        termMonths: { type: integer }
        termEndsAt: { type: [string, "null"], format: date-time }
        notes: { type: [string, "null"] }
        activatedAt: { type: [string, "null"], format: date-time, description: Billing starts here }
        suspendedAt: { type: [string, "null"], format: date-time }
        cancelledAt: { type: [string, "null"], format: date-time }
        createdAt: { type: string, format: date-time }
        sla:
          type: object
          properties:
            coverage: { type: string, enum: [BUSINESS_HOURS, TWENTY_FOUR_SEVEN] }
            calendar: { type: string, enum: [SA, TR] }
            timeZone: { type: string, example: Asia/Riyadh }
            businessHours: { type: [object, "null"], properties: { workdays: { type: array, items: { type: integer }, description: 0 is Sunday }, start: { type: string, example: "09:00" }, end: { type: string, example: "17:00" } } }
            responseTargets: { $ref: "#/components/schemas/ManagedTargets" }
            resolveTargets: { $ref: "#/components/schemas/ManagedTargets" }
        onboarding: { type: object, description: On a single contract, properties: { done: { type: integer }, total: { type: integer }, items: { type: array, items: { type: object, properties: { key: { type: string }, title: { type: string }, done: { type: boolean }, doneAt: { type: [string, "null"], format: date-time } } } } } }
        responsibilities: { type: array, description: On a single contract, items: { type: object, properties: { id: { type: string }, area: { type: string }, owner: { type: string, enum: [PROGRID, CUSTOMER, SHARED] }, notes: { type: [string, "null"] } } } }
        assets: { type: object, description: "Asset counts by status, on a single contract", additionalProperties: { type: integer } }
        usage: { type: object, description: "Engineer time this calendar month, on a single contract", properties: { periodStart: { type: string, format: date-time }, billableMinutes: { type: integer }, nonBillableMinutes: { type: integer }, includedMinutes: { type: integer }, overageMinutes: { type: integer } } }
    ManagedCloudAsset:
      type: object
      properties:
        id: { type: string }
        contractId: { type: string }
        kind: { type: string, enum: [PLATFORM_SERVER, EXTERNAL_SERVER, SITE] }
        serverId: { type: [string, "null"] }
        name: { type: string }
        address: { type: [string, "null"] }
        provider: { type: [string, "null"] }
        os: { type: [string, "null"] }
        status: { type: string, enum: [PENDING, APPROVED, REJECTED] }
        monitoringEnabled: { type: boolean }
        backupEnabled: { type: boolean }
        health: { type: string, enum: [UNKNOWN, HEALTHY, DEGRADED, UNHEALTHY] }
        lastHeartbeatAt: { type: [string, "null"], format: date-time }
        openAlerts: { type: integer }
        rejectedReason: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
    ManagedCloudTicket:
      type: object
      properties:
        id: { type: string }
        number: { type: integer }
        subject: { type: string }
        status: { type: string, enum: [open, answered, closed], description: "open waits on us, answered waits on you" }
        priority: { $ref: "#/components/schemas/ManagedPriority" }
        contractId: { type: string }
        assetId: { type: [string, "null"] }
        source: { type: [string, "null"], enum: [customer, alert, maintenance, null] }
        responseDueAt: { type: string, format: date-time }
        resolveDueAt: { type: string, format: date-time }
        firstRespondedAt: { type: [string, "null"], format: date-time }
        closedAt: { type: [string, "null"], format: date-time }
        responseBreached: { type: boolean }
        resolveBreached: { type: boolean }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        messageCount: { type: integer }
        messages: { type: array, description: Present on a single ticket, items: { type: object, properties: { id: { type: string }, fromSupport: { type: boolean }, author: { type: string }, body: { type: string }, createdAt: { type: string, format: date-time } } } }
    ManagedCloudMonthlyReport:
      type: object
      properties:
        id: { type: string }
        contractId: { type: string }
        period: { type: string, example: "2026-09" }
        status: { type: string, enum: [DRAFT, SENT] }
        recommendations: { type: string }
        data: { type: object, description: "Uptime per asset, incidents, SLA met and breached, patch runs, backup tests and hours", additionalProperties: true }
        generatedAt: { type: string, format: date-time }
        sentAt: { type: [string, "null"], format: date-time }
        pdfUrl: { type: string, description: Path of the PDF download }
    ServerAction:
      type: object
      properties:
        id: { type: string }
        serverId: { type: string }
        type: { type: string, enum: [create, start, stop, reboot, resize, rebuild, snapshot, delete] }
        status: { type: string, enum: [queued, running, completed, failed] }
        error: { type: [string, "null"] }
        startedAt: { type: string, format: date-time }
        finishedAt: { type: [string, "null"], format: date-time }
    FirewallRule:
      type: object
      required: [direction, protocol, cidrs]
      properties:
        direction: { type: string, enum: [inbound, outbound] }
        protocol: { type: string, enum: [tcp, udp, icmp, any] }
        ports: { type: string, example: "80-443" }
        cidrs: { type: array, items: { type: string }, example: ["0.0.0.0/0"] }
        description: { type: string }
    EventName:
      type: string
      enum: [server.created, server.active, server.failed, server.deleted, server.resized, snapshot.completed, invoice.issued, invoice.paid, payment.failed, spend.alert, spend.limit_reached, account.suspended]
