openapi: 3.1.0
info:
  title: BusyBoard API
  version: 1.0.0
  description: |
    REST API for programmatic access to BusyBoard availability data.
    The API exposes cross-person busy/free data while keeping event details private.
  contact:
    url: https://busyboard.app
  license:
    name: MIT

servers:
  - url: https://busyboard.app/api/v1
    description: Production

security:
  - apiKey: []

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: "API key with `bb_live_` prefix. Create at Settings > API & Integrations."

  schemas:
    Person:
      type: object
      properties:
        id: { type: string, format: uuid }
        firstName: { type: string, nullable: true }
        lastName: { type: string, nullable: true }
        email: { type: string, format: email }
        imageUrl: { type: string, nullable: true }
        timezone: { type: string, nullable: true, example: "Europe/Prague" }

    Block:
      type: object
      properties:
        id: { type: string }
        userId: { type: string, format: uuid }
        startTime: { type: string, format: date-time }
        endTime: { type: string, format: date-time }
        isAllDay: { type: boolean }
        responseStatus: { type: string, enum: [accepted, tentative, needsAction], description: "Only included for own blocks (dual-detail model)" }
        mergedCount: { type: integer }

    Slot:
      type: object
      properties:
        startTime: { type: string, format: date-time }
        endTime: { type: string, format: date-time }

    Group:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        color: { type: string, nullable: true }
        emoji: { type: string, nullable: true }
        organizationId: { type: string, format: uuid, nullable: true }
        memberCount: { type: integer }
        memberIds: { type: array, items: { type: string, format: uuid } }

    Pagination:
      type: object
      properties:
        total: { type: integer }
        limit: { type: integer }
        offset: { type: integer }
        hasMore: { type: boolean }

    Error:
      type: object
      properties:
        error: { type: string }
        code: { type: string, example: "validation_error" }

  parameters:
    limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1 }
      description: Number of items to return
    offset:
      name: offset
      in: query
      schema: { type: integer, minimum: 0, default: 0 }
      description: Number of items to skip

  headers:
    X-RateLimit-Limit:
      schema: { type: integer, example: 100 }
      description: Requests allowed per minute
    X-RateLimit-Remaining:
      schema: { type: integer }
      description: Requests remaining in current window
    X-RateLimit-Reset:
      schema: { type: integer }
      description: Unix timestamp (seconds) when the rate limit resets
    X-Request-Id:
      schema: { type: string, format: uuid }
      description: Unique request ID for tracing. Echoed if provided by client.

paths:
  /me:
    get:
      summary: Get current user profile
      operationId: getMe
      responses:
        "200":
          description: User profile with plan, limits, and usage
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  email: { type: string }
                  firstName: { type: string, nullable: true }
                  lastName: { type: string, nullable: true }
                  timezone: { type: string, nullable: true }
                  showWeekends: { type: boolean }
                  workingHours: { type: object, nullable: true }
                  plan: { type: string, enum: [free, pro] }
                  limits: { type: object }
                  usage:
                    type: object
                    properties:
                      activeCalendars: { type: integer }
                      groups: { type: integer }

  /people:
    get:
      summary: List accessible people
      operationId: getPeople
      description: Returns org members grouped by org, external connections, and a flat deduplicated list for name matching.
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
      responses:
        "200":
          description: People list with pagination on the flat `data` array
          content:
            application/json:
              schema:
                type: object
                properties:
                  orgs:
                    type: array
                    items:
                      type: object
                      properties:
                        orgId: { type: string }
                        orgName: { type: string }
                        members: { type: array, items: { $ref: "#/components/schemas/Person" } }
                  connections:
                    type: array
                    items:
                      type: object
                      properties:
                        connectionId: { type: string }
                        user: { $ref: "#/components/schemas/Person" }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Person" }
                    description: Flat deduplicated list of all people (paginated)
                  pagination: { $ref: "#/components/schemas/Pagination" }

  /groups:
    get:
      summary: List user's groups
      operationId: getGroups
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
      responses:
        "200":
          description: Groups with member counts
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Group" }
                  pagination: { $ref: "#/components/schemas/Pagination" }

  /availability:
    get:
      summary: Get busy blocks for users
      operationId: getAvailability
      description: |
        Returns busy blocks for specified users. Own blocks include full detail (responseStatus);
        other users' blocks show busy/free only (dual-detail privacy model).

        **Date interpretation:** `startDate` and `endDate` are interpreted as full calendar
        days in the API key owner's timezone (NOT UTC). For example, a Bratislava user
        passing `startDate=2026-04-08&endDate=2026-04-08` queries the entire day of
        April 8 in CET/CEST.
      parameters:
        - name: userIds
          in: query
          schema: { type: string }
          description: Comma-separated user IDs (defaults to self)
        - name: startDate
          in: query
          required: true
          schema: { type: string, format: date }
          description: YYYY-MM-DD; interpreted in the API key owner's timezone.
        - name: endDate
          in: query
          required: true
          schema: { type: string, format: date }
          description: YYYY-MM-DD; interpreted in the API key owner's timezone (inclusive end-of-day).
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
      responses:
        "200":
          description: Busy blocks with pagination
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Block" }
                  pagination: { $ref: "#/components/schemas/Pagination" }

  /availability/summary:
    get:
      summary: Get availability summary
      operationId: getAvailabilitySummary
      description: |
        Natural language summary with busyness score for the authenticated user.

        **Date interpretation:** `date` is interpreted in the API key owner's timezone
        (NOT UTC). When `range=week`, the Sunday-anchored week containing `date` is used.
      parameters:
        - name: date
          in: query
          schema: { type: string, format: date }
          description: Target date YYYY-MM-DD, interpreted in the API key owner's timezone (defaults to today).
        - name: range
          in: query
          schema: { type: string, enum: [day, week], default: day }
      responses:
        "200":
          description: Summary with busyness score
          content:
            application/json:
              schema:
                type: object
                properties:
                  range: { type: string }
                  startDate: { type: string, format: date-time }
                  endDate: { type: string, format: date-time }
                  meetingCount: { type: integer }
                  totalBusyHours: { type: number }
                  totalWorkingHours: { type: number }
                  busynessScore: { type: integer, minimum: 0, maximum: 100 }
                  nextFreeAt: { type: string, nullable: true }
                  summary: { type: string, description: Natural language summary }

  /find-time:
    get:
      summary: Find mutual free time
      operationId: findTime
      description: |
        Find available time slots when multiple people are all free.

        **Date interpretation:** `startDate` and `endDate` are interpreted as full calendar
        days in the API key owner's timezone (NOT UTC).
      parameters:
        - name: userIds
          in: query
          required: true
          schema: { type: string }
          description: Comma-separated user IDs (required)
        - name: durationMinutes
          in: query
          schema: { type: integer, minimum: 5, maximum: 480, default: 30 }
        - name: startDate
          in: query
          required: true
          schema: { type: string, format: date }
          description: YYYY-MM-DD; interpreted in the API key owner's timezone.
        - name: endDate
          in: query
          required: true
          schema: { type: string, format: date }
          description: YYYY-MM-DD; interpreted in the API key owner's timezone (inclusive end-of-day).
        - name: withinWorkingHours
          in: query
          schema: { type: boolean, default: true }
      responses:
        "200":
          description: Available time slots
          content:
            application/json:
              schema:
                type: object
                properties:
                  slots:
                    type: array
                    items: { $ref: "#/components/schemas/Slot" }
                  durationMinutes: { type: integer }
                  userIds: { type: array, items: { type: string } }
                  withinWorkingHours: { type: boolean }

  /calendars:
    get:
      summary: List connected calendar accounts
      operationId: getCalendars
      description: Returns calendar accounts with nested calendars. Never exposes OAuth tokens.
      responses:
        "200":
          description: Calendar accounts
          content:
            application/json:
              schema:
                type: object
                properties:
                  accounts:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        provider: { type: string, enum: [google, outlook, apple] }
                        email: { type: string }
                        syncStatus: { type: string, enum: [active, syncing, paused, error, disconnected] }
                        lastSyncAt: { type: string, format: date-time, nullable: true }
                        createdAt: { type: string, format: date-time }
                        calendars:
                          type: array
                          items:
                            type: object
                            properties:
                              id: { type: string }
                              name: { type: string }
                              color: { type: string, nullable: true }
                              isPrimary: { type: boolean }
                              isEnabled: { type: boolean }

  /working-hours:
    get:
      summary: Get working hours configuration
      operationId: getWorkingHours
      description: Returns working hours for self or specified users (gated by shared org membership).
      parameters:
        - name: userIds
          in: query
          schema: { type: string }
          description: Comma-separated user IDs (defaults to self)
      responses:
        "200":
          description: Working hours configurations
          content:
            application/json:
              schema:
                type: object
                properties:
                  workingHours:
                    type: array
                    items:
                      type: object
                      properties:
                        userId: { type: string }
                        workingHours: { type: object, nullable: true }
                        timezone: { type: string, nullable: true }
