openapi: 3.0.3
info:
  title: SkyVM Public API
  version: 1.0.0
  description: |
    Programmatic VM management for SkyVM customers.

    `/api/v1` is frozen. Breaking changes ship on `/v2` with a deprecation window.
    Download this spec from `/api/openapi.yaml` and pin it in CI.

    Auth: `Authorization: Bearer sk_live_...` or `sk_test_...`.
    Scopes: `vms:read`, `vms:write`, `vms:delete`, `webhooks:manage`.
    Rotation: revoke the key in Dashboard → Settings, then create a new key.
    `vmId` is a deprecated alias of `id` and will be removed in a future major version.
  contact:
    name: SkyVM
    url: https://www.skyvm.cloud/docs/api
servers:
  - url: https://www.skyvm.cloud/api/v1
    description: Production
  - url: http://localhost:3000/api/v1
    description: Local development
security:
  - bearerAuth: []
tags:
  - name: VMs
  - name: Observability
  - name: Webhooks
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        API key prefixed with `sk_live_` or `sk_test_`.
        Header: `Authorization: Bearer sk_live_...`
  schemas:
    VmStatus:
      type: string
      enum: [pending, running, stopped, error]
    ApiError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - unauthorized
                - forbidden
                - not_found
                - bad_request
                - rate_limited
                - payment_required
                - name_taken
                - conflict
                - internal_error
            message:
              type: string
            portalUrl:
              type: string
            retryAfter:
              type: integer
    Vm:
      type: object
      required: [id, name, plan, status, ipAddress, region]
      properties:
        id:
          type: string
          description: Stable numeric VM id
        vmId:
          type: string
          deprecated: true
          description: Deprecated alias of `id`
        name:
          type: string
          description: Partner-facing name. Unique per account.
        plan:
          type: string
          enum: [small, medium, large]
        status:
          $ref: "#/components/schemas/VmStatus"
        ipAddress:
          type: string
          description: Dedicated public IPv4
        region:
          type: string
          example: TX - USA
        subscriptionId:
          type: string
        cpu:
          type: string
        memory:
          type: string
        storage:
          type: string
        os:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        healthCheck:
          type: object
          nullable: true
          properties:
            port:
              type: integer
            path:
              type: string
    HealthCheck:
      type: object
      required: [port]
      properties:
        port:
          type: integer
          minimum: 1
          maximum: 65535
          example: 8090
        path:
          type: string
          default: /health
          example: /health
        timeoutSec:
          type: integer
          default: 180
          minimum: 10
          maximum: 600
    CreateVmRequest:
      type: object
      required: [plan, name, password]
      properties:
        plan:
          type: string
          enum: [small, medium, large]
        name:
          type: string
          description: Lowercase letter, then 1–61 letters, numbers, or hyphens
          example: worker-1
        password:
          type: string
        promoCode:
          type: string
        healthCheck:
          $ref: "#/components/schemas/HealthCheck"
    EnsureVmRequest:
      type: object
      properties:
        plan:
          type: string
          enum: [small, medium, large]
        password:
          type: string
        promoCode:
          type: string
        healthCheck:
          $ref: "#/components/schemas/HealthCheck"
    VmActionRequest:
      type: object
      required: [action]
      properties:
        action:
          type: string
          enum: [start, stop, restart]
    WebhookSubscription:
      type: object
      properties:
        id:
          type: string
        url:
          type: string
        events:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time
        disabledAt:
          type: string
          format: date-time
          nullable: true
  examples:
    Unauthorized:
      value:
        error:
          code: unauthorized
          message: Missing or invalid Authorization header
    Forbidden:
      value:
        error:
          code: forbidden
          message: "Missing required scope: vms:write"
    PaymentRequired:
      value:
        error:
          code: payment_required
          message: Add a payment method before creating VMs
          portalUrl: https://billing.stripe.com/p/session/test
    NameTaken:
      value:
        error:
          code: name_taken
          message: A VM named worker-1 already exists
    RateLimited:
      value:
        error:
          code: rate_limited
          message: Rate limit exceeded
          retryAfter: 60
paths:
  /vms:
    get:
      summary: List VMs or look up by name
      operationId: listVms
      tags: [VMs]
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            maximum: 50
            default: 20
        - name: cursor
          in: query
          schema:
            type: string
        - name: name
          in: query
          description: When set, returns that VM (legacy `{name}-{4 chars}` names also match) or 404
          schema:
            type: string
      responses:
        "200":
          description: Paginated list, or a single Vm when `name` is set
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/Vm"
                  - type: object
                    properties:
                      vms:
                        type: array
                        items:
                          $ref: "#/components/schemas/Vm"
                      nextCursor:
                        type: string
                        nullable: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                unauthorized:
                  $ref: "#/components/examples/Unauthorized"
        "403":
          description: Missing vms:read
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                forbidden:
                  $ref: "#/components/examples/Forbidden"
        "404":
          description: Name lookup missed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
    post:
      summary: Create VM
      operationId: createVm
      tags: [VMs]
      parameters:
        - name: Idempotency-Key
          in: header
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateVmRequest"
      responses:
        "201":
          description: VM created. `status` is `running` unless `healthCheck` is set (`pending` until guest health passes).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Vm"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                unauthorized:
                  $ref: "#/components/examples/Unauthorized"
        "402":
          description: Payment method required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                payment:
                  $ref: "#/components/examples/PaymentRequired"
        "403":
          description: Missing vms:write
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
        "409":
          description: Name already taken
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                nameTaken:
                  $ref: "#/components/examples/NameTaken"
        "429":
          description: Rate limited
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"
              examples:
                rateLimited:
                  $ref: "#/components/examples/RateLimited"
  /vms/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: Numeric id for GET/DELETE. Partner name for PUT ensure.
    get:
      summary: Get VM details
      operationId: getVm
      tags: [VMs]
      responses:
        "200":
          description: VM with optional metrics summary
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Vm"
        "400":
          description: Non-numeric id — use GET /vms?name=
        "404":
          description: Not found
    put:
      summary: Ensure or get VM by name
      operationId: ensureVm
      tags: [VMs]
      description: |
        If a VM with this exact name exists (or a legacy `{name}-{4 chars}` row), return it (200).
        If it exists on a different plan, 409 conflict.
        Otherwise create it (201). Numeric path values are rejected — ensure is by name.
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EnsureVmRequest"
      responses:
        "200":
          description: Existing VM
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Vm"
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Vm"
        "400":
          description: Numeric id used, or missing plan/password on create
        "409":
          description: Exists on a different plan
    delete:
      summary: Delete VM
      operationId: deleteVm
      tags: [VMs]
      responses:
        "200":
          description: Deleted
        "404":
          description: Not found
  /vms/{id}/action:
    post:
      summary: Start, stop, or restart VM
      operationId: vmAction
      tags: [VMs]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VmActionRequest"
      responses:
        "200":
          description: Action completed
  /vms/{id}/logs:
    get:
      summary: Tail VM application logs
      operationId: getVmLogs
      tags: [Observability]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: tail
          in: query
          schema:
            type: integer
            default: 100
        - name: search
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Log lines
  /vms/{id}/metrics:
    get:
      summary: Get VM metrics
      operationId: getVmMetrics
      tags: [Observability]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: CPU, memory, disk percentages
  /webhooks:
    get:
      summary: List webhook subscriptions
      operationId: listWebhooks
      tags: [Webhooks]
      responses:
        "200":
          description: Subscriptions
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscriptions:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookSubscription"
    post:
      summary: Create webhook subscription
      operationId: createWebhook
      tags: [Webhooks]
      description: |
        Requires `webhooks:manage`. Deliveries POST JSON
        `{ event, timestamp, data }` with `X-SkyVM-Signature: sha256=...`
        and `X-SkyVM-Timestamp`. Events include `vm.created`, `vm.ready`,
        `vm.failed`, `vm.started`, `vm.stopped`, `vm.deleted`, `metrics.threshold`.
        Prefer `vm.ready` over polling.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                events:
                  type: array
                  items:
                    type: string
      responses:
        "201":
          description: Created. `secret` is shown once.
  /webhooks/{id}:
    delete:
      summary: Disable a webhook subscription
      operationId: deleteWebhook
      tags: [Webhooks]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Disabled
        "404":
          description: Not found
