{
  "openapi": "3.1.0",
  "info": {
    "title": "PitchStation Dev Instances API",
    "version": "2026-09-15",
    "description": "Reserve, register and govern cloud workbenches (dev instances) for AI coding agents. The control plane never makes a cloud call and holds no login credential for any instance: the operator-side provisioner does the AWS work and confirms it here; each box pushes its own telemetry with a per-box report token. Requires the explicit-only `devbox.manage` capability for every mutation; team members with a plain account can read boxes shared with their team."
  },
  "servers": [
    {
      "url": "https://www.pitchstation.ai"
    }
  ],
  "security": [
    {
      "pat": []
    }
  ],
  "components": {
    "securitySchemes": {
      "pat": {
        "type": "http",
        "scheme": "bearer",
        "description": "Personal access token (pst_\u2026) or session JWT."
      },
      "reportToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "Per-box report token (dbr_<id>_\u2026), valid only for POST /api/devboxes/{id}/report."
      }
    }
  },
  "paths": {
    "/api/devboxes/sizes": {
      "get": {
        "summary": "Size classes with all-in monthly prices (compute + storage + public IPv4) and the pricing version"
      }
    },
    "/api/devboxes/agents": {
      "get": {
        "summary": "Agent profiles (Claude Code, Kimi CLI, Codex) with pinned versions and login hints"
      }
    },
    "/api/devboxes/size": {
      "post": {
        "summary": "Pure profile sizing with measurements, concurrency and catalog sizeClass/instanceType/diskGb overrides; returns proposal, chosen, all-in price and reasons"
      }
    },
    "/api/devboxes": {
      "get": {
        "summary": "List owned/team-visible dev instances (all non-deleted instances for admins), with cost-to-date and the caller's committed monthly cost"
      },
      "post": {
        "summary": "Reserve a dev instance BEFORE any cloud call: name, sizeClass, arch, agents, repo, policy. Checks the capability, name uniqueness and your monthly cost cap in one transaction; returns the bootstrap URL."
      }
    },
    "/api/devboxes/{id}": {
      "get": {
        "summary": "Detail: sizing, price, policy, last report, events, cost-to-date"
      },
      "patch": {
        "summary": "Policy only: idleMinutes (15\u2013720), autoscale (never|ask|auto). Not name, not the cost cap."
      },
      "delete": {
        "summary": "Tombstone. Refused (409 INSTANCE_LIVE) while an instance id is attached and the box is not terminated."
      }
    },
    "/api/devboxes/{id}/bootstrap": {
      "get": {
        "summary": "Rendered cloud-init user-data for this reservation: pinned installers, reporter, no secrets. text/plain, or gzip over 16 KB."
      }
    },
    "/api/devboxes/{id}/provisioning": {
      "post": {
        "summary": "Provisioner reports the instance id the moment it exists (starts the 30-minute lease)"
      }
    },
    "/api/devboxes/{id}/token": {
      "post": {
        "summary": "Issue or rotate the report token (rotate-on-call; shown once). First call moves provisioning \u2192 configuring."
      }
    },
    "/api/devboxes/{id}/finalize": {
      "post": {
        "summary": "AWS facts + sshd assertions; configuring \u2192 ready. Idempotent on instance id."
      }
    },
    "/api/devboxes/{id}/report": {
      "post": {
        "summary": "Telemetry intake from the box (report token). Response carries the current policy.",
        "security": [
          {
            "reportToken": []
          }
        ]
      }
    },
    "/api/devboxes/{id}/repo": {
      "put": {
        "summary": "Assign/change repository with a required fresh scanner profile. Server recomputes class/disk, ignores caller requiredClass and returns the full sizing proposal on 409 RESIZE_REQUIRED; optional version guard."
      }
    },
    "/api/devboxes/{id}/env": {
      "put": {
        "summary": "Replace the encrypted environment (vendor keys, repo token); responses carry key names only"
      }
    },
    "/api/devboxes/{id}/measurements": {
      "put": {
        "summary": "Persist a successful operator warm-up's peakRssGb, diskUsedGb and repoHead; return measurement-driven sizing. Owner/admin with devbox.manage."
      }
    },
    "/api/devboxes/{id}/env/reveal": {
      "post": {
        "summary": "Explicit owner-only plaintext environment reveal; non-owner admins and team members are denied. Private/no-store response; audit contains key names only. Requires devbox.manage."
      }
    },
    "/api/devboxes/{id}/started": {
      "post": {
        "summary": "Operator confirmation after aws ec2 start-instances (compare-and-set; Idempotency-Key honoured)"
      }
    },
    "/api/devboxes/{id}/eip-released": {
      "post": {
        "summary": "Confirm explicit release of a stopped box's exact allocationId after operator AWS success; version-checked/idempotent. Start allocates and confirms a new EIP. Never an automatic release."
      }
    },
    "/api/devboxes/{id}/stopped": {
      "post": {
        "summary": "Operator confirmation after a manual stop or reconcile"
      }
    },
    "/api/devboxes/{id}/resizing": {
      "post": {
        "summary": "Operator begins a resize; re-runs the cost-cap check for the target class"
      }
    },
    "/api/devboxes/{id}/resized": {
      "post": {
        "summary": "Owner confirms the approved resize and verified filesystem; optional recovered address pair is stored atomically",
        "description": "Requires a matching pending resize intent. publicIp and eipAllocId must be provided together; omission preserves the stored address. Version conflicts do not consume intent. Replaying Idempotency-Key returns the original result without duplicate resize/address events. Recover a released EIP before SSH using --reconcile --repair NAME; do not use /started to bypass a pending resize.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "sizeClass": { "type": "string", "enum": ["XS", "S", "M", "L", "XL"] },
                  "arch": { "type": "string", "enum": ["arm64", "x86_64"] },
                  "diskGb": { "type": "integer" },
                  "verifiedDf": { "type": "string" },
                  "state": { "type": "string", "enum": ["ready", "running", "stopped"] },
                  "version": { "type": "integer" },
                  "publicIp": { "type": "string", "format": "ipv4" },
                  "eipAllocId": { "type": "string", "pattern": "^eipalloc-[a-z0-9]+$" }
                }
              }
            }
          }
        }
      }
    },
    "/api/devboxes/{id}/renamed": {
      "post": {
        "summary": "Operator confirms a rename; name/target label/version/event update atomically, with version checks and Idempotency-Key replay"
      }
    },
    "/api/devboxes/{id}/terminated": {
      "post": {
        "summary": "Operator confirms termination (instance and EIP gone); row kept for audit"
      }
    },
    "/api/devboxes/{id}/cap-override": {
      "post": {
        "summary": "Admin: allow the next resize on this box to exceed the owner's cap once (recorded event)"
      }
    },
    "/api/devboxes/{id}/teams": {
      "post": {
        "summary": "Share read-only visibility with a team you belong to"
      }
    },
    "/api/devboxes/{id}/teams/{teamId}": {
      "delete": {
        "summary": "Remove a team's visibility"
      }
    },
    "/api/devboxes/{id}/proposal": {
      "get": {
        "summary": "The open resize proposal from the monitor (OOM \u2192 one class up, disk > 90 % \u2192 volume +50 %), with the exact devbox.py command"
      }
    }
  }
}
