openapi: 3.0.3
info:
  title: SOAT Quotas API
  version: 1.0.0
  description: API for managing quotas and rate limits (Quotas resource)
  contact:
    name: SOAT Team
    url: https://github.com/ttoss/soat
servers:
  - url: '{baseUrl}'
    description: Base URL of your SOAT deployment (e.g. https://your-soat.com or http://localhost:5047)
    variables:
      baseUrl:
        description: The base URL of your SOAT deployment
        default: http://localhost:5047
tags:
  - name: Quotas
    description: Manage quotas and rate limits
security:
  - bearerAuth: []
paths:
  /api/v1/quotas:
    get:
      tags:
        - Quotas
      summary: List quotas
      description: Returns the quotas defined in a project
      operationId: listQuotas
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - $ref: './pagination.yaml#/components/parameters/Limit'
        - $ref: './pagination.yaml#/components/parameters/Offset'
      responses:
        '200':
          description: List of quotas
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Quota'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
    post:
      tags:
        - Quotas
      summary: Create a quota
      description: >-
        Creates a project-scoped quota. `requests` is valid for `scope:
        project`/`api_key`; `tokens` and `cost_usd` are valid for `scope:
        project`/`agent`/`actor`; `storage_bytes` is valid for `scope: project`
        only. Any other scope/metric pair is rejected with 400 (no attribution
        exists to enforce it). An `actor` quota caps one end user's spend,
        matched from the generation's session; a null `scope_ref` means one
        budget *per* actor rather than a pooled project total. A `cost_usd`
        quota may name one `meter_type` to cap; omitting it caps every priced
        meter. A duplicate quota (same project, scope, scope_ref, metric,
        window, meter_type) is rejected with 409.


        `storage_bytes` caps a stored total rather than a windowed one, so it
        takes `window: current` and every other metric refuses that value (400
        either way). It is enforced at the corpus write paths — file upload and
        create, document create, document ingest and re-ingest, memory
        create — with `409 QUOTA_STORAGE_EXCEEDED` and no `Retry-After`, since
        no window reset clears a footprint.
      operationId: createQuota
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - scope
                - metric
                - window
                - limit
              additionalProperties: false
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: Project ID (required if not using project key auth)
                  example: proj_V1StGXR8Z5jdHi6B
                scope:
                  type: string
                  enum: [project, api_key, agent, actor]
                  description: The scope the quota applies to
                scope_ref:
                  type: string
                  nullable: true
                  description: >-
                    Public id of the api key / agent / actor the quota applies
                    to. For `api_key` and `agent` scope, NULL means all entities
                    of that scope type in the project. For `actor` scope, NULL
                    means one budget *per* actor — each end user gets their own
                    allowance — rather than a pooled total across all actors.
                  example: key_V1StGXR8Z5jdHi6B
                metric:
                  type: string
                  enum: [requests, tokens, cost_usd, storage_bytes]
                  description: The metric being capped
                  example: requests
                window:
                  type: string
                  enum:
                    [
                      rolling_1m,
                      rolling_1h,
                      rolling_24h,
                      calendar_month,
                      current,
                    ]
                  description: >-
                    The window over which the metric is aggregated. `current`
                    is the only accepted value for storage_bytes (a stored
                    total is not aggregated over time) and is refused for
                    every other metric.
                limit:
                  type: number
                  description: >-
                    The cap. Must be a positive integer for
                    requests/tokens/storage_bytes (bytes); fractional values
                    are allowed for cost_usd.
                  example: 600
                mode:
                  type: string
                  enum: [enforce, monitor]
                  default: enforce
                  description: >-
                    enforce blocks with 429 (requests at the middleware,
                    tokens/cost_usd at the pre-generation check); monitor
                    observes without blocking — a breach fires the
                    quotas.exceeded webhook and writes a quotas:MonitorBreach
                    audit entry, but the request is let through.
                  example: enforce
                on_unpriced:
                  type: string
                  enum: [block, allow]
                  default: block
                  description: >-
                    Only for metric cost_usd (400 on any other metric). What an
                    enforce quota does when the current window is a pricing
                    blackout — several metered llm_tokens events, none of them
                    priced, so the aggregate is 0 however much was actually
                    spent. Platform meters such as compute_execution are read
                    for the aggregate but never for this verdict. block
                    (the default) refuses new generations with 409
                    QUOTA_UNENFORCEABLE until pricing is configured; allow
                    accepts the unmeasurable spend explicitly. Either way a
                    quota_unpriced exception is filed. monitor-mode quotas
                    never block regardless. A partly priced window is not a
                    blackout: no posture refuses it, it is enforced on its
                    priced total, and it files the same exception.
                  example: block
                meter_type:
                  type: string
                  enum: [llm_tokens, compute_execution, api_request, storage, tool_execution]
                  description: >-
                    Only for metric cost_usd (400 on any other metric). The
                    meter this cap answers for. Omit it and the cap sums every
                    priced meter, which is the existing behaviour; name one and
                    only that meter's cost counts, so an AI spend cap is not
                    consumed by platform meters the operator prices (and vice
                    versa). Part of the quota's identity, so two meter scopes
                    can share a scope/metric/window and neither conflicts with
                    an unscoped cap. Immutable after creation — replace the
                    quota to change it.
      responses:
        '201':
          description: Quota created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Quota'
        '400':
          description: Bad request (invalid scope/metric/window/mode/limit)
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '409':
          description: A matching quota already exists
        '500':
          description: Internal server error
  /api/v1/quotas/{quota_id}:
    get:
      tags:
        - Quotas
      summary: Get a quota
      description: Returns a specific quota, including current window usage
      operationId: getQuota
      x-soat-resource:
        kind: quota
        from: quota_id
      parameters:
        - name: quota_id
          in: path
          required: true
          description: Quota ID
          schema:
            type: string
            example: quota_V1StGXR8Z5jdHi6B
      responses:
        '200':
          description: Quota details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Quota'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Quota not found
    patch:
      tags:
        - Quotas
      summary: Update a quota
      description: Updates a quota's limit and/or mode. Other fields are immutable.
      operationId: updateQuota
      x-soat-resource:
        kind: quota
        from: quota_id
      parameters:
        - name: quota_id
          in: path
          required: true
          description: Quota ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                limit:
                  type: number
                  description: New limit
                  example: 1000
                mode:
                  type: string
                  enum: [enforce, monitor]
                  description: New mode
                  example: monitor
                on_unpriced:
                  type: string
                  enum: [block, allow]
                  description: >-
                    New pricing posture. Only for metric cost_usd (400 on any
                    other metric); see the create operation for what block and
                    allow mean.
                  example: allow
      responses:
        '200':
          description: Quota updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Quota'
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Quota not found
    delete:
      tags:
        - Quotas
      summary: Delete a quota
      description: Deletes a quota and drops its window counters
      operationId: deleteQuota
      x-soat-resource:
        kind: quota
        from: quota_id
      parameters:
        - name: quota_id
          in: path
          required: true
          description: Quota ID
          schema:
            type: string
      responses:
        '204':
          description: Quota deleted successfully
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Quota not found
components:
  schemas:
    Quota:
      type: object
      properties:
        id:
          type: string
          example: quota_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
        scope:
          type: string
          enum: [project, api_key, agent, actor]
        scope_ref:
          type: string
          nullable: true
          description: >-
            Public id of the api key / agent / actor the quota applies to. For
            `api_key` and `agent` scope, NULL means all entities of that scope
            type in the project. For `actor` scope, NULL means one budget *per*
            actor rather than a pooled total across all actors.
        metric:
          type: string
          enum: [requests, tokens, cost_usd, storage_bytes]
        window:
          type: string
          enum: [rolling_1m, rolling_1h, rolling_24h, calendar_month, current]
          description: >-
            The window the metric is aggregated over; `current` on
            storage_bytes, which caps a stored total and never resets.
        limit:
          type: number
        mode:
          type: string
          enum: [enforce, monitor]
        meter_type:
          type: string
          enum:
            [llm_tokens, compute_execution, api_request, storage, tool_execution, null]
          nullable: true
          description: >-
            The meter a cost_usd cap answers for. Null is every priced meter,
            which is what a quota created without one carries.
        on_unpriced:
          type: string
          enum: [block, allow, null]
          nullable: true
          description: >-
            Pricing posture of a cost_usd quota over an unpriced blackout —
            block refuses generations, allow lets them through (the
            quota_unpriced exception is filed either way, and for a partly
            priced window, which no posture refuses). Null for metrics
            with no pricing dependency.
        current_usage:
          type: object
          nullable: true
          description: >-
            Current fixed-window usage for the requests metric. Null for
            token/cost quotas (which aggregate the usage meter at check time
            rather than keeping a counter), null for storage_bytes (a stored
            total has no window and no counter — read the footprint from the
            storage meter), and null in list responses.
          properties:
            window_key:
              type: string
              example: '2026-07-07T12:31Z'
            count:
              type: integer
              example: 42
            resets_at:
              type: string
              format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: JWT token or sk_ api key
