openapi: "3.0.3"

info:
  title: SentryAgent.ai — Organizations (Multi-Tenancy)
  version: 1.0.0
  description: |
    Organization (tenant) management endpoints for the SentryAgent.ai AgentIdP platform.

    Organizations are the top-level multi-tenancy boundary. Each organization has its
    own pool of registered agents, plan-tier limits, and billing context.

    **All endpoints require a valid Bearer JWT** with appropriate OPA-enforced scope.

    **Plan Tiers:**
    | Tier | Max Agents | Max Tokens/Month |
    |------|-----------|-----------------|
    | `free` | 100 | 10,000 |
    | `pro` | 1,000 | 100,000 |
    | `enterprise` | unlimited | unlimited |

servers:
  - url: http://localhost:3000/api/v1
    description: Local development server
  - url: https://api.sentryagent.ai/v1
    description: Production server

tags:
  - name: Organizations
    description: CRUD operations for organizations (tenants)
  - name: Organization Members
    description: Agent membership management within organizations

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT access token obtained via `POST /token`.
        Include as `Authorization: Bearer <token>`.

  schemas:
    PlanTier:
      type: string
      enum:
        - free
        - pro
        - enterprise
      description: Subscription plan tier for an organization.
      example: pro

    OrgStatus:
      type: string
      enum:
        - active
        - suspended
        - deleted
      description: Lifecycle status of an organization.
      example: active

    OrgRole:
      type: string
      enum:
        - member
        - admin
      description: Role of an agent within an organization.
      example: member

    Organization:
      type: object
      description: Full representation of a registered organization (tenant).
      required:
        - organizationId
        - name
        - slug
        - planTier
        - maxAgents
        - maxTokensPerMonth
        - status
        - createdAt
        - updatedAt
      properties:
        organizationId:
          type: string
          format: uuid
          description: Immutable, system-assigned unique identifier for the organization.
          readOnly: true
          example: "org-1234-5678-abcd-ef01"
        name:
          type: string
          description: Human-readable display name of the organization.
          minLength: 1
          maxLength: 256
          example: "Acme Corp"
        slug:
          type: string
          description: |
            URL-friendly unique identifier. Lowercase letters, digits, and hyphens only.
            Immutable after creation.
          pattern: '^[a-z0-9-]+$'
          example: "acme-corp"
        planTier:
          $ref: '#/components/schemas/PlanTier'
        maxAgents:
          type: integer
          description: Maximum number of agents permitted in this organization.
          minimum: 1
          example: 100
        maxTokensPerMonth:
          type: integer
          description: Maximum OAuth 2.0 token requests per calendar month.
          minimum: 1
          example: 10000
        status:
          $ref: '#/components/schemas/OrgStatus'
        createdAt:
          type: string
          format: date-time
          readOnly: true
          example: "2026-03-01T08:00:00.000Z"
        updatedAt:
          type: string
          format: date-time
          readOnly: true
          example: "2026-03-28T11:30:00.000Z"

    CreateOrgRequest:
      type: object
      description: Request body for creating a new organization.
      required:
        - name
        - slug
      properties:
        name:
          type: string
          description: Human-readable display name.
          minLength: 1
          maxLength: 256
          example: "Acme Corp"
        slug:
          type: string
          description: URL-friendly unique identifier. Lowercase letters, digits, hyphens only.
          pattern: '^[a-z0-9-]+$'
          minLength: 1
          maxLength: 64
          example: "acme-corp"
        planTier:
          $ref: '#/components/schemas/PlanTier'
          description: Defaults to `free` when omitted.
        maxAgents:
          type: integer
          description: Override the default max agents for this plan tier.
          minimum: 1
          example: 100
        maxTokensPerMonth:
          type: integer
          description: Override the default max tokens per month.
          minimum: 1
          example: 10000

    UpdateOrgRequest:
      type: object
      description: |
        Request body for partially updating an organization.
        All fields are optional; only provided fields are updated.
        `status` may only be set to `active` or `suspended` via this endpoint;
        `deleted` is applied only through the DELETE operation.
      minProperties: 1
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 256
          example: "Acme Corporation"
        planTier:
          $ref: '#/components/schemas/PlanTier'
        maxAgents:
          type: integer
          minimum: 1
          example: 1000
        maxTokensPerMonth:
          type: integer
          minimum: 1
          example: 100000
        status:
          type: string
          enum:
            - active
            - suspended
          description: Set to `active` to reactivate a suspended org, or `suspended` to disable it.
          example: active

    PaginatedOrgsResponse:
      type: object
      description: Paginated list of organizations.
      required:
        - data
        - total
        - page
        - limit
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Organization'
        total:
          type: integer
          example: 42
        page:
          type: integer
          example: 1
        limit:
          type: integer
          example: 20

    OrgMember:
      type: object
      description: An agent's membership record within an organization.
      required:
        - memberId
        - organizationId
        - agentId
        - role
        - joinedAt
      properties:
        memberId:
          type: string
          format: uuid
          readOnly: true
          example: "mem-abcd-1234-5678-ef01"
        organizationId:
          type: string
          format: uuid
          example: "org-1234-5678-abcd-ef01"
        agentId:
          type: string
          format: uuid
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        role:
          $ref: '#/components/schemas/OrgRole'
        joinedAt:
          type: string
          format: date-time
          readOnly: true
          example: "2026-03-28T09:00:00.000Z"

    AddMemberRequest:
      type: object
      description: Request body for adding an agent to an organization.
      required:
        - agentId
        - role
      properties:
        agentId:
          type: string
          format: uuid
          description: UUID of the agent to add.
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        role:
          $ref: '#/components/schemas/OrgRole'

    ErrorResponse:
      type: object
      description: Standard error response envelope.
      required:
        - code
        - message
      properties:
        code:
          type: string
          example: "VALIDATION_ERROR"
        message:
          type: string
          example: "Request validation failed."
        details:
          type: object
          additionalProperties: true

  responses:
    Unauthorized:
      description: Missing or invalid Bearer token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: "UNAUTHORIZED"
            message: "A valid Bearer token is required to access this resource."

    Forbidden:
      description: Valid token but insufficient permissions.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: "FORBIDDEN"
            message: "You do not have permission to perform this action."

    NotFound:
      description: Organization not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: "ORG_NOT_FOUND"
            message: "Organization with the specified ID was not found."

    TooManyRequests:
      description: Rate limit exceeded.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: "RATE_LIMIT_EXCEEDED"
            message: "Too many requests. Please retry after the rate limit window resets."

    InternalServerError:
      description: Unexpected server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: "INTERNAL_SERVER_ERROR"
            message: "An unexpected error occurred. Please try again later."

security:
  - BearerAuth: []

paths:
  /organizations:
    post:
      operationId: createOrganization
      tags:
        - Organizations
      summary: Create a new organization
      description: |
        Creates a new organization (tenant). The `slug` must be unique across all organizations
        and is immutable after creation.

        The requesting agent's organization context is determined from the Bearer token.
        Requires `admin:orgs` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrgRequest'
            example:
              name: "Acme Corp"
              slug: "acme-corp"
              planTier: "pro"
              maxAgents: 500
              maxTokensPerMonth: 50000
      responses:
        '201':
          description: Organization created successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Organization'
              example:
                organizationId: "org-1234-5678-abcd-ef01"
                name: "Acme Corp"
                slug: "acme-corp"
                planTier: "pro"
                maxAgents: 500
                maxTokensPerMonth: 50000
                status: "active"
                createdAt: "2026-04-07T09:00:00.000Z"
                updatedAt: "2026-04-07T09:00:00.000Z"
        '400':
          description: Validation error in request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "VALIDATION_ERROR"
                message: "Request validation failed."
                details:
                  field: "slug"
                  reason: "slug must contain only lowercase letters, digits, and hyphens."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: An organization with the provided slug already exists.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "ORG_SLUG_CONFLICT"
                message: "An organization with this slug already exists."
                details:
                  slug: "acme-corp"
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

    get:
      operationId: listOrganizations
      tags:
        - Organizations
      summary: List organizations
      description: |
        Returns a paginated list of organizations. Results can be filtered by `status`.
        Results are ordered by `createdAt` descending.

        Requires `admin:orgs` scope.
      parameters:
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          example: 1
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          example: 20
        - name: status
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/OrgStatus'
      responses:
        '200':
          description: Paginated list of organizations returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedOrgsResponse'
              example:
                data:
                  - organizationId: "org-1234-5678-abcd-ef01"
                    name: "Acme Corp"
                    slug: "acme-corp"
                    planTier: "pro"
                    maxAgents: 500
                    maxTokensPerMonth: 50000
                    status: "active"
                    createdAt: "2026-03-01T08:00:00.000Z"
                    updatedAt: "2026-03-28T11:30:00.000Z"
                total: 42
                page: 1
                limit: 20
        '400':
          description: Invalid query parameters.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "VALIDATION_ERROR"
                message: "Invalid query parameter value."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /organizations/{orgId}:
    parameters:
      - name: orgId
        in: path
        required: true
        description: The unique UUID identifier of the organization.
        schema:
          type: string
          format: uuid
        example: "org-1234-5678-abcd-ef01"

    get:
      operationId: getOrganization
      tags:
        - Organizations
      summary: Get organization by ID
      description: |
        Retrieves the full record for a single organization.
        Requires `admin:orgs` scope.
      responses:
        '200':
          description: Organization record returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Organization'
              example:
                organizationId: "org-1234-5678-abcd-ef01"
                name: "Acme Corp"
                slug: "acme-corp"
                planTier: "pro"
                maxAgents: 500
                maxTokensPerMonth: 50000
                status: "active"
                createdAt: "2026-03-01T08:00:00.000Z"
                updatedAt: "2026-03-28T11:30:00.000Z"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalServerError'

    patch:
      operationId: updateOrganization
      tags:
        - Organizations
      summary: Update organization metadata
      description: |
        Partially updates an organization's metadata.
        Only provided fields are updated; omitted fields are unchanged.
        `slug` and `organizationId` are immutable.
        Requires `admin:orgs` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateOrgRequest'
            example:
              name: "Acme Corporation"
              planTier: "enterprise"
      responses:
        '200':
          description: Organization updated successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Organization'
              example:
                organizationId: "org-1234-5678-abcd-ef01"
                name: "Acme Corporation"
                slug: "acme-corp"
                planTier: "enterprise"
                maxAgents: 500
                maxTokensPerMonth: 50000
                status: "active"
                createdAt: "2026-03-01T08:00:00.000Z"
                updatedAt: "2026-04-07T09:00:00.000Z"
        '400':
          description: Validation error or attempt to modify an immutable field.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "VALIDATION_ERROR"
                message: "Request validation failed."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

    delete:
      operationId: deleteOrganization
      tags:
        - Organizations
      summary: Soft-delete an organization
      description: |
        Permanently soft-deletes an organization by setting its status to `deleted`.
        The record is retained for audit purposes.

        **Effects:**
        - All agents within the organization are suspended.
        - No new tokens may be issued for agents in this organization.
        - This operation is **irreversible** via the API.

        Requires `admin:orgs` scope.
      responses:
        '204':
          description: Organization soft-deleted successfully. No response body.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Organization is already deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "ORG_ALREADY_DELETED"
                message: "This organization has already been deleted."
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /organizations/{orgId}/members:
    parameters:
      - name: orgId
        in: path
        required: true
        description: The unique UUID identifier of the organization.
        schema:
          type: string
          format: uuid
        example: "org-1234-5678-abcd-ef01"

    post:
      operationId: addOrganizationMember
      tags:
        - Organization Members
      summary: Add an agent as a member of an organization
      description: |
        Adds a registered agent to an organization with the specified role.
        The agent must already be registered in the system.

        Requires `admin:orgs` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddMemberRequest'
            example:
              agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              role: "member"
      responses:
        '201':
          description: Agent added to organization successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrgMember'
              example:
                memberId: "mem-abcd-1234-5678-ef01"
                organizationId: "org-1234-5678-abcd-ef01"
                agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                role: "member"
                joinedAt: "2026-04-07T09:00:00.000Z"
        '400':
          description: Validation error in request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "VALIDATION_ERROR"
                message: "Request validation failed."
                details:
                  field: "role"
                  reason: "role must be one of: member, admin."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Organization or agent not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                orgNotFound:
                  summary: Organization not found
                  value:
                    code: "ORG_NOT_FOUND"
                    message: "Organization with the specified ID was not found."
                agentNotFound:
                  summary: Agent not found
                  value:
                    code: "AGENT_NOT_FOUND"
                    message: "Agent with the specified ID was not found."
        '409':
          description: Agent is already a member of this organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "ALREADY_MEMBER"
                message: "The agent is already a member of this organization."
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalServerError'
