openapi: 3.0.3
info:
  title: SOAT Approvals API
  version: 1.0.0
  description: >-
    API for the approvals queue — human-decision items proposed by the platform
    (an `approval` orchestration node, or tool-call interception). Items are
    created by the platform only; there is no public create endpoint. Every item
    carries a frozen proposed action, its supporting evidence, and a
    server-enforced expiry after which it can never execute.
  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: Approvals
    description: Manage the human-decision approval queue
security:
  - bearerAuth: []
paths:
  /api/v1/approvals:
    get:
      tags:
        - Approvals
      summary: List approval items
      description: Returns approval items for a project, filterable by status, origin, and expiry.
      operationId: listApprovals
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: status
          in: query
          description: Filter by lifecycle status
          schema:
            type: string
            enum: [pending, approved, rejected, expired]
        - name: origin
          in: query
          description: Filter by producer origin
          schema:
            type: string
            enum: [node, tool_call, task_transition]
        - name: expires_before
          in: query
          description: Return only items expiring at or before this timestamp
          schema:
            type: string
            format: date-time
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of approval items
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ApprovalItem'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '400':
          description: Invalid `status` or `origin` filter value
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
  /api/v1/approvals/recurrences:
    get:
      tags:
        - Approvals
      summary: List recurring approval groups
      description: >-
        Read-only rollup answering "what keeps coming back?" — groups items by
        `dedup_key` and returns those recurring at least `min_count` times,
        most-recurrent first. Each group carries the ordered item chain (via
        `previous_item_id`) and the resolution reasons in order, so a human can
        read recurring rejections side by side and graduate the pattern into a
        guardrail `deny`. Exact-key grouping only; no cluster state is stored.
      operationId: listApprovalRecurrences
      parameters:
        - name: project_id
          in: query
          description: Project ID (required if not using project key auth)
          schema:
            type: string
            example: proj_V1StGXR8Z5jdHi6B
        - name: status
          in: query
          description: Lifecycle status the groups are built from (default `rejected`)
          schema:
            type: string
            enum: [pending, approved, rejected, expired]
            default: rejected
        - name: min_count
          in: query
          description: Minimum items in a group for it to be returned
          schema:
            type: integer
            default: 2
            minimum: 1
        - name: limit
          in: query
          required: false
          description: Maximum number of groups to return
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of groups to skip
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of recurrence groups
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ApprovalRecurrenceGroup'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '400':
          description: Invalid `status` filter value
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '500':
          description: Internal server error
  /api/v1/approvals/{approval_id}:
    get:
      tags:
        - Approvals
      summary: Get an approval item
      description: Returns a single approval item with its full evidence.
      operationId: getApproval
      parameters:
        - $ref: '#/components/parameters/approval_id'
      responses:
        '200':
          description: Approval item
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalItem'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Approval item not found
  /api/v1/approvals/{approval_id}/approve:
    post:
      tags:
        - Approvals
      summary: Approve an approval item
      description: >-
        Approves the item. Optionally supply edited `arguments` to replace the
        proposed arguments (edit-then-approve); the original is preserved on the
        item. Expiry is re-checked at decision time — an expired item can never
        be approved.
      operationId: approveApproval
      parameters:
        - $ref: '#/components/parameters/approval_id'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                arguments:
                  type: object
                  description: Edited arguments to execute instead of the proposed ones
      responses:
        '200':
          description: Approval item approved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalItem'
        '400':
          description: Edited arguments are not a JSON object
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Approval item not found
        '409':
          description: Item already resolved or expired
  /api/v1/approvals/{approval_id}/reject:
    post:
      tags:
        - Approvals
      summary: Reject an approval item
      description: Rejects the item. A reason is required and preserved on the item.
      operationId: rejectApproval
      parameters:
        - $ref: '#/components/parameters/approval_id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - reason
              properties:
                reason:
                  type: string
                  description: Why the item is being rejected (required)
                  example: Amount exceeds the approved monthly budget.
      responses:
        '200':
          description: Approval item rejected
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalItem'
        '400':
          description: Reason missing
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Approval item not found
        '409':
          description: Item already resolved or expired
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: JWT token or sk_ api key
  parameters:
    approval_id:
      name: approval_id
      in: path
      required: true
      description: Approval item ID
      schema:
        type: string
        example: apr_V1StGXR8Z5jdHi6B
  schemas:
    ApprovalRecurrenceGroup:
      type: object
      description: >-
        A set of approval items sharing a `dedup_key` — the same proposed action
        recurring — with the ordered chain and its resolution reasons.
      properties:
        dedup_key:
          type: string
          description: The shared dedup key that defines the group
        agent_id:
          type: string
          nullable: true
          description: Proposing agent (shared across the group)
        tool_id:
          type: string
          nullable: true
          description: Proposed tool (shared across the group)
        count:
          type: integer
          description: Number of items in the group
        chain:
          type: array
          description: The items oldest → newest (the `previous_item_id` chain)
          items:
            type: object
            properties:
              id:
                type: string
                example: apr_V1StGXR8Z5jdHi6B
              status:
                type: string
                enum: [pending, approved, rejected, expired]
              resolution_reason:
                type: string
                nullable: true
              created_at:
                type: string
                format: date-time
        reasons:
          type: array
          description: The chain's resolution reasons in order (empty entries omitted)
          items:
            type: string
    ApprovalItem:
      type: object
      properties:
        id:
          type: string
          example: apr_V1StGXR8Z5jdHi6B
        project_id:
          x-soat-ref: projects
          type: string
        origin:
          type: string
          enum: [node, tool_call, task_transition]
          description: How the item was produced (analytics/filtering only)
        status:
          type: string
          enum: [pending, approved, rejected, expired]
        proposed_action:
          type: object
          nullable: true
          description: >-
            The frozen proposed action. Null for producers whose proposal is not
            a tool call (a `task_transition` item gates a workflow transition,
            named by `task_transition`).
          properties:
            tool_id:
              type: string
            action:
              type: string
              description: >-
                Resolved action name (the builtin/mcp action) for `tool_call`-origin
                items — always present there, even for single-action tools.
                Omitted for `node`-origin items, whose downstream execution is
                wired by a separate `tool` node in the graph.
            arguments:
              type: object
        reasoning:
          type: string
          nullable: true
          description: The proposing agent's rationale
        evidence:
          type: object
          nullable: true
          description: Supporting structured data
        predicted_impact:
          type: string
          nullable: true
          description: Expected execution effect
        expires_at:
          type: string
          format: date-time
          description: Server-enforced hard gate; the item can never execute after this
        dedup_key:
          type: string
          nullable: true
          description: Set on tool-call items to suppress duplicate proposals
        orchestration_run_id:
          type: string
          nullable: true
          description: Originating orchestration run (node producer)
        node_id:
          type: string
          nullable: true
          description: Originating node id within the run's graph
        generation_id:
          type: string
          nullable: true
          description: Originating generation (tool-call producer)
        session_id:
          type: string
          nullable: true
          description: Session the originating generation ran in (tool-call producer)
        agent_id:
          type: string
          nullable: true
          description: Proposing agent
        task_id:
          type: string
          nullable: true
          description: Gated task (task_transition producer)
        task_transition:
          type: string
          nullable: true
          description: Transition fired on approval (task_transition producer)
        policy_version:
          type: string
          nullable: true
        previous_item_id:
          type: string
          nullable: true
          description: >-
            Prior item's ID when this proposal was re-filed after an earlier
            matching item (same dedup_key) had been rejected
          example: apr_V1StGXR8Z5jdHi6B
        resolved_by:
          type: string
          nullable: true
          description: Resolving user's public ID; null on expiry
        resolution_reason:
          type: string
          nullable: true
          description: Required on rejection
        edited_arguments:
          type: object
          nullable: true
          description: Set on edit-then-approve
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
