openapi: 3.1.0
info:
  title: Rateplane Public API
  version: "1.0.0"
  summary: Multi-cloud pricing catalog and FinOps automation surface
  description: |
    Rateplane exposes two surfaces:

    - **Public catalog** — searchable, normalized compute/region/pricing
      data across AWS, Azure, GCP. No authentication required.
    - **Workspace API** — agent and integration endpoints scoped to a
      single workspace. Authenticated via a workspace-scoped Bearer
      token (issued at `/dashboard/api-keys`, prefix `rp_live_`).

    This spec covers the supported, stable surface. Internal cron,
    webhook, and admin routes are intentionally undocumented and
    subject to change without notice.

  contact:
    name: Rateplane Support
    email: support@rateplane.com
    url: https://rateplane.com/docs
  license:
    name: Proprietary

servers:
  - url: https://app.rateplane.com
    description: Production
  - url: http://localhost:3000
    description: Local development

tags:
  - name: catalog
    description: Public multi-cloud pricing catalog. No authentication.
  - name: agent
    description: Agent-facing tools (function-calling shape). Requires API key.
  - name: account
    description: Authenticated user / workspace endpoints.
  - name: budgets
    description: Workspace budgets — monthly cost ceilings with email alerts.
  - name: alerts
    description: Price alerts for catalog instances.
  - name: accounts
    description: Connected cloud accounts (AWS / Azure / GCP credentials).
  - name: api-keys
    description: Workspace API key lifecycle.

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: rp_live_...
      description: |
        Workspace-scoped API key. Issue at `/dashboard/api-keys`. The
        raw key is only shown once at creation; pass it as
        `Authorization: Bearer rp_live_xxx`. Keys carry a single
        workspace and inherit the issuing user's permissions.

  parameters:
    Provider:
      name: provider
      in: query
      required: false
      schema:
        type: string
        enum: [aws, azure, gcp, oci, linode, vultr]
      description: Restrict results to one provider's catalog rows.
    Region:
      name: region
      in: query
      required: false
      schema:
        type: string
      description: Region/zone slug (e.g. `us-east-1`, `eastus`, `us-central1`).
    Search:
      name: q
      in: query
      required: false
      schema:
        type: string
      description: Free-text search over instance / family names.
    MinVcpus:
      name: minVcpus
      in: query
      required: false
      schema: { type: integer, minimum: 0 }
    MaxVcpus:
      name: maxVcpus
      in: query
      required: false
      schema: { type: integer, minimum: 0 }
    MinMemoryGb:
      name: minMemoryGb
      in: query
      required: false
      schema: { type: number, minimum: 0 }
    MaxMemoryGb:
      name: maxMemoryGb
      in: query
      required: false
      schema: { type: number, minimum: 0 }
    MinHourly:
      name: minHourly
      in: query
      required: false
      schema: { type: number, minimum: 0 }
    MaxHourly:
      name: maxHourly
      in: query
      required: false
      schema: { type: number, minimum: 0 }
    PriceType:
      name: priceType
      in: query
      required: false
      schema:
        type: string
        enum: [ON_DEMAND, SPOT, RESERVED_1YR, RESERVED_3YR, SAVINGS_PLAN_1YR, SAVINGS_PLAN_3YR]
        default: ON_DEMAND
    FamilyCategory:
      name: familyCategory
      in: query
      required: false
      schema:
        type: string
        enum: [GENERAL, COMPUTE, MEMORY, STORAGE, GPU, ACCELERATOR]
    ServiceCategory:
      name: serviceCategory
      in: query
      required: false
      schema:
        type: string
        enum: [COMPUTE, STORAGE, DATABASE, NETWORK, GPU]
    Page:
      name: page
      in: query
      required: false
      schema: { type: integer, minimum: 1, default: 1 }
    PageSize:
      name: pageSize
      in: query
      required: false
      schema: { type: integer, minimum: 10, maximum: 100, default: 50 }

  schemas:
    Provenance:
      type: object
      properties:
        source: { type: string, example: "AWS EC2 pricing" }
        sourceType:
          type: string
          enum: [authoritative, community, seed, synthetic]
        sourceUrl: { type: string, format: uri }
        fetchedAt: { type: string, format: date-time }
        effectiveAt: { type: string, format: date-time }
        staleAfter: { type: string, format: date-time }
      required: [source, sourceType, fetchedAt]

    Pricing:
      type: object
      properties:
        onDemand: { type: number, nullable: true }
        spot: { type: number, nullable: true }
        reserved1yr: { type: number, nullable: true }
        reserved3yr: { type: number, nullable: true }
        savingsPlan1yr: { type: number, nullable: true }
        savingsPlan3yr: { type: number, nullable: true }
        currency: { type: string, example: USD }
        operatingSystem: { type: string, example: Linux }

    Instance:
      type: object
      properties:
        id: { type: string }
        name: { type: string, example: "m5.large" }
        provider: { type: string, enum: [AWS, AZURE, GCP, OCI, LINODE, VULTR] }
        family: { type: string, example: "m5" }
        familyCategory:
          type: string
          enum: [GENERAL, COMPUTE, MEMORY, STORAGE, GPU, ACCELERATOR]
        vcpus: { type: integer }
        memoryGb: { type: number }
        architecture: { type: string, example: x86_64 }
        gpuCount: { type: integer }
        gpuName: { type: string, nullable: true }
        region:
          type: object
          properties:
            name: { type: string, example: us-east-1 }
            displayName: { type: string }
        pricing:
          $ref: "#/components/schemas/Pricing"
        provenance:
          $ref: "#/components/schemas/Provenance"
      required: [id, name, provider, family, vcpus, memoryGb, pricing]

    InstanceCatalogResponse:
      type: object
      properties:
        success: { type: boolean }
        source:
          type: string
          description: Internal source tag indicating which catalog backend served the response.
        total: { type: integer }
        page: { type: integer }
        pageSize: { type: integer }
        totalPages: { type: integer }
        data:
          type: array
          items:
            $ref: "#/components/schemas/Instance"

    AgentToolSpec:
      type: object
      properties:
        name: { type: string }
        description: { type: string }
        parameters:
          type: object
          description: JSON Schema for the tool's input parameters.

    AgentInvokeRequest:
      type: object
      required: [name]
      properties:
        name: { type: string, description: Tool name (from /api/agent/tools GET response) }
        arguments:
          type: object
          description: Arguments matching the tool's `parameters` JSON Schema.

    AgentInvokeResponse:
      type: object
      properties:
        result:
          description: Tool return value. Shape depends on the tool.

    ApiError:
      type: object
      required: [success, error]
      properties:
        success: { type: boolean, enum: [false] }
        error: { type: string }

  responses:
    Unauthorized:
      description: Missing or invalid bearer token.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    BadRequest:
      description: Invalid request payload.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    InternalError:
      description: Server error.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }

paths:
  /api/instances:
    get:
      tags: [catalog]
      summary: Search the multi-cloud instance catalog
      description: |
        Returns normalized instance + pricing rows across providers.
        Filterable by provider, region, vCPUs, memory, hourly cost,
        family category, and service category.

        When the live catalog DB is unavailable and a static snapshot
        fallback is enabled, the response includes a `note` indicating
        the snapshot is hidden from this endpoint to prevent stale
        data from leaking into procurement workflows.
      parameters:
        - $ref: "#/components/parameters/Provider"
        - $ref: "#/components/parameters/Region"
        - $ref: "#/components/parameters/Search"
        - $ref: "#/components/parameters/MinVcpus"
        - $ref: "#/components/parameters/MaxVcpus"
        - $ref: "#/components/parameters/MinMemoryGb"
        - $ref: "#/components/parameters/MaxMemoryGb"
        - $ref: "#/components/parameters/MinHourly"
        - $ref: "#/components/parameters/MaxHourly"
        - $ref: "#/components/parameters/PriceType"
        - $ref: "#/components/parameters/FamilyCategory"
        - $ref: "#/components/parameters/ServiceCategory"
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PageSize"
      responses:
        "200":
          description: Catalog rows.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InstanceCatalogResponse" }
        "500":
          $ref: "#/components/responses/InternalError"

  /api/regions:
    get:
      tags: [catalog]
      summary: List regions for a provider
      parameters:
        - $ref: "#/components/parameters/Provider"
      responses:
        "200":
          description: Regions.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        name: { type: string }
                        displayName: { type: string }
                        country: { type: string }
                        continent: { type: string }
                        provider: { type: string }

  /api/compare:
    get:
      tags: [catalog]
      summary: Side-by-side compare for an instance across providers
      parameters:
        - name: name
          in: query
          required: true
          schema: { type: string }
          description: Instance name (e.g. `m5.large`).
        - $ref: "#/components/parameters/PriceType"
      responses:
        "200":
          description: Comparable instance set.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Instance" }

  /api/agent/tools:
    get:
      tags: [agent]
      summary: List agent tool specs (JSON-Schema function-calling shape)
      security:
        - apiKey: []
      responses:
        "200":
          description: Tool list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  tools:
                    type: array
                    items: { $ref: "#/components/schemas/AgentToolSpec" }
        "401":
          $ref: "#/components/responses/Unauthorized"

    post:
      tags: [agent]
      summary: Invoke an agent tool
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AgentInvokeRequest" }
      responses:
        "200":
          description: Tool result.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AgentInvokeResponse" }
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/account/export:
    post:
      tags: [account]
      summary: GDPR Article 20 data portability export
      description: |
        Returns a JSON dump of every record the authenticated user is
        entitled to under GDPR / DPA 2018: the user row, workspace
        memberships, owned-workspace data (with credential fields
        scrubbed), and AuditEvent rows for actions they took.

        Hard-capped at 5000 rows per collection. The export is itself
        audit-logged.
      security:
        - apiKey: []
      responses:
        "200":
          description: JSON export.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/account/delete:
    delete:
      tags: [account]
      summary: GDPR right-to-erasure — permanently delete the user
      description: |
        Permanently removes the authenticated user's account and all
        associated workspace data (cascade-deleted by Prisma). A
        pseudonymised log line is emitted to the structured logs so
        operators can confirm the deletion happened without retaining
        the user's email.
      security:
        - apiKey: []
      responses:
        "200":
          description: Deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, enum: [true] }
        "500":
          $ref: "#/components/responses/InternalError"

  /api/budgets:
    get:
      tags: [budgets]
      summary: List workspace budgets
      security:
        - apiKey: []
      responses:
        "200":
          description: Budgets in the workspace.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        name: { type: string }
                        amount: { type: number }
                        alertAt: { type: number, minimum: 0.1, maximum: 1 }
                        provider: { type: string, nullable: true, enum: [AWS, AZURE, GCP, OCI] }
                        createdAt: { type: string, format: date-time }
                        updatedAt: { type: string, format: date-time }
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [budgets]
      summary: Create a budget
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, amount]
              properties:
                name: { type: string, minLength: 1, maxLength: 80 }
                amount: { type: number, exclusiveMinimum: 0 }
                alertAt: { type: number, minimum: 0.1, maximum: 1, default: 0.8 }
                provider: { type: string, enum: [AWS, AZURE, GCP, OCI] }
      responses:
        "200":
          description: Budget created.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Plan budget limit exceeded.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }

  /api/budgets/{id}:
    delete:
      tags: [budgets]
      summary: Delete a budget
      security:
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Deleted.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Budget not found.

  /api/alerts:
    get:
      tags: [alerts]
      summary: List price alerts
      security:
        - apiKey: []
      responses:
        "200":
          description: Alerts in the workspace.
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [alerts]
      summary: Create a price alert
      security:
        - apiKey: []
      responses:
        "200": { description: Created. }
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/alerts/{id}:
    delete:
      tags: [alerts]
      summary: Delete a price alert
      security:
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200": { description: Deleted. }
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/accounts:
    get:
      tags: [accounts]
      summary: List connected cloud accounts (credentials scrubbed)
      security:
        - apiKey: []
      responses:
        "200": { description: Connected cloud accounts. }
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [accounts]
      summary: Connect a new cloud account
      description: |
        Encrypts AWS/Azure/GCP credentials at rest using envelope
        encryption. Audit-logged as `account.connect`.
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [provider, name]
              properties:
                provider: { type: string, enum: [AWS, AZURE, GCP, OCI] }
                name: { type: string }
      responses:
        "200": { description: Connected. }
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Plan account limit exceeded.

  /api/accounts/{id}:
    delete:
      tags: [accounts]
      summary: Disconnect a cloud account
      security:
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200": { description: Disconnected. }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Account not found.

  /api/api-keys:
    get:
      tags: [api-keys]
      summary: List API keys for the workspace (hashes never returned)
      security:
        - apiKey: []
      responses:
        "200": { description: API keys. }
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [api-keys]
      summary: Create a new API key (raw key returned once — store it now)
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, minLength: 1, maxLength: 80 }
                expiresAt: { type: string, format: date-time }
      responses:
        "200":
          description: |
            Created. The `key` field in the response is the only time
            the raw token is exposed; subsequent reads only return
            metadata.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: API access requires the Pro plan or higher.
