Docs
OpenAPI specification

OpenAPI specification

Raw YAML for the TenantX HTTP API. Import into Swagger UI, Redoc, or your API client.

Large file — use your editor or an OpenAPI viewer for the best experience.

openapi: 3.1.0
info:
  title: Tenantx API
  version: "1.0"
  description: |
    Tenantx is a multi-tenant B2B SaaS starter kit (Laravel 12 + React 18).

    ## Authentication
    All protected endpoints require a **Bearer token** in the `Authorization` header.
    Obtain a token via `POST /api/auth/login`.

    ## Tenancy model
    ```
    Platform Admin (global subscription.admin)
      └── Organization  (billing + permission boundary)
            └── Workspace (workspace_branding row; legacy name)
                  └── User
    ```

    ## Base URL
    Default local: `http://localhost:8000/api`

  contact:
    name: Tenantx
  license:
    name: MIT

servers:
  - url: http://localhost:8000/api
    description: Local development

tags:
  - name: Auth
    description: Authentication and account management
  - name: Password Reset
    description: Self-service password reset flow
  - name: 2FA
    description: TOTP two-factor authentication
  - name: Organizations
    description: Organization CRUD (tenant-scoped)
  - name: Users
    description: User management within an organization
  - name: Workspaces
    description: Workspace (workspace_branding) management
  - name: Invitations
    description: Team invitation flow
  - name: Permissions
    description: Spatie permission + role management
  - name: Roles
    description: Role management
  - name: Help Center
    description: Help center articles and categories
  - name: Notifications
    description: In-app notification management
  - name: Activity Logs
    description: Audit activity log (tenantx_logs schema, PostgreSQL only)
  - name: Dashboard
    description: Dashboard statistics
  - name: Subscription
    description: Organization subscription, plans, usage
  - name: Search
    description: Global search within workspace
  - name: API Tokens
    description: Personal API token management
  - name: System Settings
    description: Organization-scoped system settings
  - name: Platform Admin
    description: Platform admin surface (requires subscription.admin)
  - name: Public
    description: Public endpoints — no authentication required

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    Profile:
      type: object
      properties:
        id: { type: string, format: uuid }
        email: { type: string, format: email }
        full_name: { type: string, nullable: true }
        role: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        avatar_url: { type: string, nullable: true }
        is_active: { type: boolean }
        organization_id: { type: string, format: uuid, nullable: true }
        default_workspace_id:
          {
            type: string,
            format: uuid,
            nullable: true,
            description: "User's default workspace UUID",
          }
        workspaces_access_all:
          { type: boolean, description: "Can switch workspace context" }

    Organization:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        slug: { type: string }
        settings: { type: object }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    Workspace:
      type: object
      description: "Workspace branding row (stored in workspace_branding table)"
      properties:
        id: { type: string, format: uuid }
        organization_id: { type: string, format: uuid }
        name: { type: string, description: "Workspace display name" }
        email: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        created_at: { type: string, format: date-time }

    Invitation:
      type: object
      properties:
        id: { type: string, format: uuid }
        email: { type: string, format: email }
        role: { type: string, example: member }
        invited_by_user_id: { type: string, format: uuid, nullable: true }
        expires_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }

    Notification:
      type: object
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        body: { type: string, nullable: true }
        type: { type: string }
        is_read: { type: boolean }
        url: { type: string, nullable: true }
        created_at: { type: string, format: date-time }

    Permission:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        guard_name: { type: string }
        organization_id: { type: string, format: uuid, nullable: true }

    Role:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        guard_name: { type: string }
        organization_id: { type: string, format: uuid, nullable: true }

    SubscriptionPlan:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        slug: { type: string }
        price: { type: number }
        currency: { type: string }
        features: { type: array, items: { type: string } }

    HelpCenterArticle:
      type: object
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        slug: { type: string }
        body: { type: string }
        category_id: { type: string, format: uuid }
        language: { type: string, example: en }
        status: { type: string, enum: [draft, published, archived] }
        is_global: { type: boolean }

    ApiToken:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        last_used_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }

    Error:
      type: object
      properties:
        message: { type: string }
        errors:
          {
            type: object,
            additionalProperties: { type: array, items: { type: string } },
          }

security:
  - BearerAuth: []

paths:
  # ──────────────────────────────────────────────────────────────────────────
  # Public — no auth
  # ──────────────────────────────────────────────────────────────────────────

  /health:
    get:
      tags: [Public]
      summary: Health check
      security: []
      responses:
        "200":
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  service: { type: string, example: Tenantx API }

  /auth/login:
    post:
      tags: [Auth]
      summary: Sign in
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string, format: email }
                password: { type: string }
      responses:
        "200":
          description: Authenticated
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: { type: string }
                  user: { type: object }
                  profile: { $ref: "#/components/schemas/Profile" }
        "422":
          description: Invalid credentials or account locked
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /auth/register:
    post:
      tags: [Auth]
      summary: Self-serve registration (creates org + first admin user)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                [
                  email,
                  password,
                  password_confirmation,
                  full_name,
                  organization_name,
                ]
              properties:
                email: { type: string, format: email }
                password: { type: string, minLength: 8 }
                password_confirmation: { type: string }
                full_name: { type: string }
                organization_name: { type: string }
      responses:
        "201": { description: Account created, token returned }
        "422": { description: Validation error }

  /auth/forgot-password:
    post:
      tags: [Password Reset]
      summary: Request a password reset link (always returns 200 to prevent enumeration)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
      responses:
        "200":
          description: Reset link sent (if email exists)
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }

  /auth/reset-password:
    post:
      tags: [Password Reset]
      summary: Reset password using token from email link (token valid 60 min)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, token, password, password_confirmation]
              properties:
                email: { type: string, format: email }
                token: { type: string }
                password: { type: string, minLength: 8 }
                password_confirmation: { type: string }
      responses:
        "200": { description: Password reset — all Sanctum tokens revoked }
        "422": { description: Invalid / expired token }

  /auth/two-factor/verify:
    post:
      tags: [2FA]
      summary: Verify TOTP code during login challenge
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [login_token, code]
              properties:
                login_token: { type: string }
                code: { type: string, minLength: 6, maxLength: 8 }
      responses:
        "200": { description: Authenticated, token returned }
        "422": { description: Invalid code }

  /invitations/accept:
    get:
      tags: [Invitations]
      summary: Get invitation metadata (public — no auth)
      security: []
      parameters:
        - in: query
          name: token
          required: true
          schema: { type: string }
        - in: query
          name: email
          required: true
          schema: { type: string, format: email }
      responses:
        "200":
          description: Invitation details
          content:
            application/json:
              schema:
                type: object
                properties:
                  email: { type: string }
                  role: { type: string }
                  organization:
                    {
                      type: object,
                      properties:
                        { id: { type: string }, name: { type: string } },
                    }
                  invited_by: { type: string, nullable: true }
                  expires_at: { type: string, format: date-time }
        "422": { description: Invalid or expired invitation }

    post:
      tags: [Invitations]
      summary: Accept invitation — creates account if needed, returns auth token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, email, password, password_confirmation]
              properties:
                token: { type: string }
                email: { type: string, format: email }
                password: { type: string, minLength: 8 }
                password_confirmation: { type: string }
                full_name: { type: string }
      responses:
        "201":
          description: Account created/joined, auth token returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  token: { type: string }
        "422": { description: Invalid or expired invitation }

  /subscription/plans:
    get:
      tags: [Subscription]
      summary: List public subscription plans
      security: []
      responses:
        "200": { description: Plans list }

  # ──────────────────────────────────────────────────────────────────────────
  # Authenticated — auth:sanctum + organization
  # ──────────────────────────────────────────────────────────────────────────

  /auth/logout:
    post:
      tags: [Auth]
      summary: Revoke current token
      responses:
        "200": { description: Logged out }

  /auth/profile:
    get:
      tags: [Auth]
      summary: Get current user profile
      responses:
        "200":
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Profile" }
    put:
      tags: [Auth]
      summary: Update current user profile
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                full_name: { type: string }
                phone: { type: string }
      responses:
        "200": { description: Updated profile }

  /auth/change-password:
    post:
      tags: [Auth]
      summary: Change own password (throttle 5/min)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                [current_password, new_password, new_password_confirmation]
              properties:
                current_password: { type: string }
                new_password: { type: string, minLength: 8 }
                new_password_confirmation: { type: string }
      responses:
        "200": { description: Password changed }
        "422": { description: Wrong current password }

  /auth/two-factor/status:
    get:
      tags: [2FA]
      summary: Get 2FA status for current user
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  enabled: { type: boolean }
                  confirmed: { type: boolean }

  /auth/two-factor/setup:
    post:
      tags: [2FA]
      summary: Generate TOTP secret and QR code URI
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  secret: { type: string }
                  qr_code:
                    {
                      type: string,
                      description: "otpauth:// URI for QR code generation",
                    }

  /auth/two-factor/confirm:
    post:
      tags: [2FA]
      summary: Confirm 2FA setup with a valid TOTP code
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string, minLength: 6, maxLength: 8 }
      responses:
        "200": { description: 2FA enabled }
        "422": { description: Invalid code }

  /auth/two-factor/disable:
    post:
      tags: [2FA]
      summary: Disable 2FA
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password: { type: string }
      responses:
        "200": { description: 2FA disabled }

  /organizations:
    get:
      tags: [Organizations]
      summary: List organizations accessible to current user
      responses:
        "200":
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Organization" }
    post:
      tags: [Organizations]
      summary: Create a new organization (throttle 20/min)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, slug]
              properties:
                name: { type: string }
                slug: { type: string }
      responses:
        "201":
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Organization" }

  /organizations/{id}:
    get:
      tags: [Organizations]
      summary: Get organization by ID
      parameters:
        - {
            name: id,
            in: path,
            required: true,
            schema: { type: string, format: uuid },
          }
      responses:
        "200":
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Organization" }
    put:
      tags: [Organizations]
      summary: Update organization
      parameters:
        - {
            name: id,
            in: path,
            required: true,
            schema: { type: string, format: uuid },
          }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                settings: { type: object }
      responses:
        "200": { description: Updated }
    delete:
      tags: [Organizations]
      summary: Delete organization
      parameters:
        - {
            name: id,
            in: path,
            required: true,
            schema: { type: string, format: uuid },
          }
      responses:
        "200": { description: Deleted }

  /users:
    get:
      tags: [Users]
      summary: List users in current organization
      parameters:
        - { name: search, in: query, schema: { type: string } }
        - { name: role, in: query, schema: { type: string } }
        - { name: is_active, in: query, schema: { type: boolean } }
      responses:
        "200":
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Profile" }
    post:
      tags: [Users]
      summary: Create a user in this organization
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password, full_name, role]
              properties:
                email: { type: string, format: email }
                password: { type: string, minLength: 8 }
                full_name: { type: string }
                role: { type: string }
                phone: { type: string }
      responses:
        "201": { description: User created }

  /users/{id}:
    get:
      tags: [Users]
      summary: Get user by ID
      parameters:
        - {
            name: id,
            in: path,
            required: true,
            schema: { type: string, format: uuid },
          }
      responses:
        "200":
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Profile" }
    put:
      tags: [Users]
      summary: Update user
      parameters:
        - {
            name: id,
            in: path,
            required: true,
            schema: { type: string, format: uuid },
          }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                full_name: { type: string }
                role: { type: string }
                is_active: { type: boolean }
      responses:
        "200": { description: Updated }
    delete:
      tags: [Users]
      summary: Delete user
      parameters:
        - {
            name: id,
            in: path,
            required: true,
            schema: { type: string, format: uuid },
          }
      responses:
        "200": { description: Deleted }

  /invitations:
    get:
      tags: [Invitations]
      summary: List pending invitations for the current organization
      responses:
        "200":
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Invitation" }
    post:
      tags: [Invitations]
      summary: Send a team invitation by email
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                role: { type: string, default: member }
      responses:
        "201":
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Invitation" }
        "422": { description: Email already in org or invalid }

  /invitations/{id}:
    delete:
      tags: [Invitations]
      summary: Cancel a pending invitation
      parameters:
        - {
            name: id,
            in: path,
            required: true,
            schema: { type: string, format: uuid },
          }
      responses:
        "200": { description: Invitation cancelled }
        "404": { description: Not found }

  /workspaces:
    get:
      tags: [Workspaces]
      summary: List workspaces for current organization
      responses:
        "200":
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Workspace" }
    post:
      tags: [Workspaces]
      summary: Create a workspace
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                email: { type: string, format: email }
                phone: { type: string }
      responses:
        "201": { description: Workspace created }

  /workspaces/{workspace}:
    get:
      tags: [Workspaces]
      summary: Get workspace by ID
      parameters:
        - {
            name: workspace,
            in: path,
            required: true,
            schema: { type: string, format: uuid },
          }
      responses:
        "200":
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Workspace" }
    put:
      tags: [Workspaces]
      summary: Update workspace
      parameters:
        - {
            name: workspace,
            in: path,
            required: true,
            schema: { type: string, format: uuid },
          }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                email: { type: string }
      responses:
        "200": { description: Updated }
    delete:
      tags: [Workspaces]
      summary: Delete workspace (requires multi_workspace feature)
      parameters:
        - {
            name: workspace,
            in: path,
            required: true,
            schema: { type: string, format: uuid },
          }
      responses:
        "200": { description: Deleted }

  /permissions/user:
    get:
      tags: [Permissions]
      summary: Get current user's permissions (used by frontend useUserPermissions hook)
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  permissions:
                    type: array
                    items: { type: string }

  /permissions:
    get:
      tags: [Permissions]
      summary: List all permissions for current organization
      responses:
        "200":
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Permission" }

  /roles:
    get:
      tags: [Roles]
      summary: List roles for current organization
      responses:
        "200":
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Role" }
    post:
      tags: [Roles]
      summary: Create a role
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
      responses:
        "201": { description: Role created }

  /notifications:
    get:
      tags: [Notifications]
      summary: List notifications for current user
      parameters:
        - { name: page, in: query, schema: { type: integer } }
        - { name: per_page, in: query, schema: { type: integer } }
      responses:
        "200":
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Notification" }

  /notifications/unread-count:
    get:
      tags: [Notifications]
      summary: Get unread notification count
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  count: { type: integer }

  /notifications/read-all:
    post:
      tags: [Notifications]
      summary: Mark all notifications as read
      responses:
        "200": { description: All marked read }

  /activity-logs:
    get:
      tags: [Activity Logs]
      summary: List activity logs for current workspace (PostgreSQL only; requires workspace context)
      parameters:
        - {
            name: workspace_id,
            in: query,
            required: true,
            schema: { type: string, format: uuid },
            description: "Workspace UUID (legacy param name)",
          }
        - { name: page, in: query, schema: { type: integer } }
        - { name: per_page, in: query, schema: { type: integer } }
        - { name: log_name, in: query, schema: { type: string } }
        - { name: event, in: query, schema: { type: string } }
      responses:
        "200": { description: Paginated activity logs }

  /dashboard/stats:
    get:
      tags: [Dashboard]
      summary: Workspace-scoped dashboard statistics
      parameters:
        - {
            name: workspace_id,
            in: query,
            required: true,
            schema: { type: string, format: uuid },
          }
      responses:
        "200": { description: Stats object }

  /organization-dashboard/overview:
    get:
      tags: [Dashboard]
      summary: Organization-wide dashboard overview (total workspaces, users, recent activity)
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  kernel: { type: string, example: tenantx }
                  summary: { type: object }
                  workspaces: { type: array }

  /search:
    get:
      tags: [Search]
      summary: Search users, organizations, workspaces, and help center articles
      parameters:
        - { name: q, in: query, required: true, schema: { type: string } }
        - {
            name: workspace_id,
            in: query,
            schema: { type: string, format: uuid },
          }
      responses:
        "200": { description: Search results grouped by type }

  /api-tokens:
    get:
      tags: [API Tokens]
      summary: List personal API tokens for current user
      responses:
        "200":
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ApiToken" }
    post:
      tags: [API Tokens]
      summary: Create a new personal API token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
      responses:
        "201":
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: integer }
                  name: { type: string }
                  plain_token:
                    { type: string, description: "Shown once — store securely" }
                  created_at: { type: string, format: date-time }

  /api-tokens/{token}:
    delete:
      tags: [API Tokens]
      summary: Revoke a personal API token
      parameters:
        - { name: token, in: path, required: true, schema: { type: integer } }
      responses:
        "200": { description: Token revoked }

  /organization-system-settings:
    get:
      tags: [System Settings]
      summary: Get organization system settings JSON blob
      parameters:
        - {
            name: workspace_id,
            in: query,
            schema: { type: string, format: uuid },
          }
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  settings: { type: object }
    put:
      tags: [System Settings]
      summary: Update organization system settings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                settings: { type: object }
      responses:
        "200": { description: Settings saved }

  /subscription/status:
    get:
      tags: [Subscription]
      summary: Get current organization subscription status
      responses:
        "200": { description: Subscription status }

  /subscription/usage:
    get:
      tags: [Subscription]
      summary: Get subscription usage (users, workspaces vs plan limits)
      responses:
        "200": { description: Usage object }

  /subscription/features:
    get:
      tags: [Subscription]
      summary: Get features available on current plan
      responses:
        "200": { description: Feature map }

  # ──────────────────────────────────────────────────────────────────────────
  # Platform Admin — auth:sanctum + platform.admin (subscription.admin required)
  # ──────────────────────────────────────────────────────────────────────────

  /platform/dashboard:
    get:
      tags: [Platform Admin]
      summary: Platform admin dashboard overview
      responses:
        "200": { description: Platform metrics }
        "403": { description: Not a platform admin }

  /platform/organizations:
    get:
      tags: [Platform Admin]
      summary: List all organizations
      responses:
        "200": { description: Organizations list }
    post:
      tags: [Platform Admin]
      summary: Create an organization as platform admin
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, slug]
              properties:
                name: { type: string }
                slug: { type: string }
      responses:
        "201": { description: Organization created }

  /platform/organizations/{organizationId}/activate:
    post:
      tags: [Platform Admin]
      summary: Activate an organization subscription
      parameters:
        - {
            name: organizationId,
            in: path,
            required: true,
            schema: { type: string, format: uuid },
          }
      responses:
        "200": { description: Activated }

  /platform/organizations/{organizationId}/suspend:
    post:
      tags: [Platform Admin]
      summary: Suspend an organization subscription
      parameters:
        - {
            name: organizationId,
            in: path,
            required: true,
            schema: { type: string, format: uuid },
          }
      responses:
        "200": { description: Suspended }

  /platform/plans:
    get:
      tags: [Platform Admin]
      summary: List subscription plans
      responses:
        "200": { description: Plans list }
    post:
      tags: [Platform Admin]
      summary: Create a subscription plan
      responses:
        "201": { description: Plan created }

  /platform/login-audit:
    get:
      tags: [Platform Admin]
      summary: List login audit records
      responses:
        "200": { description: Login audit records }

  /platform/maintenance/status:
    get:
      tags: [Platform Admin]
      summary: Get maintenance mode status
      responses:
        "200": { description: Maintenance status }

  /platform/maintenance/enable:
    post:
      tags: [Platform Admin]
      summary: Enable maintenance mode
      responses:
        "200": { description: Maintenance enabled }

  /platform/maintenance/disable:
    post:
      tags: [Platform Admin]
      summary: Disable maintenance mode
      responses:
        "200": { description: Maintenance disabled }

  /platform/users:
    get:
      tags: [Platform Admin]
      summary: List all platform users
      responses:
        "200": { description: Users list }
    post:
      tags: [Platform Admin]
      summary: Create a platform user
      responses:
        "201": { description: User created }