openapi: 3.1.0

info:
  title: Every API
  version: 1.1.0
  description: |
    REST API for Every, an agency operations platform. Work is organised as
    Clients → Projects → Tasks, with time tracking, retainer budgets, and
    capacity reporting.

    ## Authentication

    Two token types are accepted; both are sent as `Authorization: Bearer <token>`.

    - **Personal access tokens (PAT)** — minted from your account settings with
      granular scopes. Best for scripts and integrations.
    - **OAuth2 Authorization Code Grant with PKCE** — for user-facing clients.

    ### OAuth2 Flow

    1. Generate a random `code_verifier` and derive a `code_challenge` (S256).
    2. Redirect the user to the authorization URL with the required parameters.
    3. After the user authorizes, exchange the authorization code for an access token at the token URL.
    4. Use the access token in the `Authorization: Bearer <token>` header for all API requests.
    5. Refresh the access token using the refresh URL before it expires.

    ### Token Lifetimes

    - Access tokens: 15 days
    - Refresh tokens: 30 days
    - Personal access tokens: 6 months

    ### Browser Extension Client

    - Client ID: `019d083f-77ef-7363-995f-50d908e11825` (public client, PKCE)
    - Redirect URI: `https://extensions.wip.test/callback`

    ## Conventions

    - List endpoints paginate 50 per page; pass `page` to move through results.
    - Unknown request fields are ignored by validation — check field names here
      (e.g. `task_status_id`, `label_ids`, `department_ids`, plural `_ids` arrays of integers).
    - Durations are written like `30m`, `2h`, `1h30m`, `1.5h`, or `3d`
      (days use the agency's working-hours-per-day setting), or send integer
      `duration_minutes` instead.

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

security:
  - oauth2: []

paths:
  /api/v1/user:
    get:
      operationId: getAuthenticatedUser
      summary: Get authenticated user
      description: Returns the profile of the currently authenticated user.
      tags:
        - User
      security:
        - oauth2:
            - user:read
      responses:
        "200":
          description: Authenticated user profile.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/User"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/users:
    get:
      operationId: listUsers
      summary: List team members
      description: Returns a paginated list of team members. Use the numeric `id` for assignee fields and the `uuid` for @-mentions.
      tags:
        - User
      security:
        - oauth2:
            - users:read
      responses:
        "200":
          description: Paginated list of users.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedUsers"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/clients:
    get:
      operationId: listClients
      summary: List clients
      description: Returns a paginated list of clients visible to the authenticated user. Filter with `search`.
      tags:
        - Clients
      security:
        - oauth2:
            - clients:read
      parameters:
        - name: search
          in: query
          description: Filter clients by name.
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Paginated list of clients.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedClients"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"

    post:
      operationId: createClient
      summary: Create a client
      tags:
        - Clients
      security:
        - oauth2:
            - clients:create
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - task_prefix
              properties:
                name:
                  type: string
                  maxLength: 255
                task_prefix:
                  type: string
                  maxLength: 10
                  description: Uppercase prefix for task references (unique across clients and projects).
                  example: GO
                contact_name:
                  type: string
                contact_email:
                  type: string
                  format: email
                phone:
                  type: string
                address:
                  type: string
                notes:
                  type: string
                hourly_rate:
                  type: number
      responses:
        "201":
          description: Client created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Client"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/ValidationError"

  /api/v1/projects:
    get:
      operationId: listProjects
      summary: List projects
      description: Returns a paginated list of projects accessible to the authenticated user.
      tags:
        - Projects
      security:
        - oauth2:
            - projects:read
      responses:
        "200":
          description: Paginated list of projects.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedProjects"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"

    post:
      operationId: createProject
      summary: Create a project
      tags:
        - Projects
      security:
        - oauth2:
            - projects:create
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - client_id
                - name
              properties:
                client_id:
                  type: integer
                name:
                  type: string
                  maxLength: 255
                description:
                  type: string
                task_prefix:
                  type: string
                  maxLength: 10
                  description: Optional uppercase prefix for this project's task references.
      responses:
        "201":
          description: Project created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Project"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/ValidationError"

  /api/v1/task-statuses:
    get:
      operationId: listTaskStatuses
      summary: List task statuses
      description: Returns the workspace task statuses (workflow columns) in board order. Use `id` as `task_status_id` when creating or updating tasks.
      tags:
        - Workspace
      security:
        - oauth2:
            - task-statuses:read
      responses:
        "200":
          description: Task statuses.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/TaskStatusFull"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/departments:
    get:
      operationId: listDepartments
      summary: List departments
      description: Returns all departments. Use IDs in the `department_ids` array on tasks.
      tags:
        - Workspace
      security:
        - oauth2:
            - departments:read
      responses:
        "200":
          description: Departments.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Department"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/labels:
    get:
      operationId: listLabels
      summary: List labels
      description: Returns all task labels. Use IDs in the `label_ids` array on tasks.
      tags:
        - Workspace
      security:
        - oauth2:
            - labels:read
      responses:
        "200":
          description: Labels.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Label"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/tasks:
    get:
      operationId: listTasks
      summary: List tasks
      description: Returns a paginated list of tasks. Optionally filter by project or assignment.
      tags:
        - Tasks
      security:
        - oauth2:
            - tasks:read
      parameters:
        - name: project_id
          in: query
          description: Filter tasks by project ID.
          required: false
          schema:
            type: integer
        - name: assigned_to_me
          in: query
          description: When true, only return tasks assigned to the authenticated user.
          required: false
          schema:
            type: boolean
      responses:
        "200":
          description: Paginated list of tasks.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedTasks"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"

    post:
      operationId: createTask
      summary: Create a task
      description: >-
        Creates a new task. Either `project_id` or `client_id` must be
        provided — or `parent_task_id` to create a subtask, which inherits
        both from its parent. Omitting `task_status_id` uses the workspace
        default status.
      tags:
        - Tasks
      security:
        - oauth2:
            - tasks:create
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTaskRequest"
      responses:
        "201":
          description: Task created successfully.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Task"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/ValidationError"

  /api/v1/tasks/{task}/convert-to-parent:
    post:
      operationId: convertTaskToParent
      summary: Break a task into subtasks
      description: >-
        Converts an ordinary task into a parent container. Its status,
        assignees, estimate, and logged time move onto an auto-created first
        subtask; comments, attachments, and history stay on the parent. The
        parent keeps its reference and disappears from boards and lists, and
        its estimate becomes the sum of its subtasks. Drafts, existing
        parents, and subtasks cannot be converted.
      tags:
        - Tasks
      security:
        - oauth2:
            - tasks:update
      parameters:
        - $ref: "#/components/parameters/TaskId"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                first_subtask_title:
                  type: string
                  minLength: 3
                  maxLength: 255
                  description: Title for the auto-created first subtask. Defaults to the task's own title.
      responses:
        "200":
          description: The converted parent task, including its subtasks.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Task"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"

  /api/v1/tasks/{task}:
    get:
      operationId: getTask
      summary: Get a task
      description: Returns a single task by its ID.
      tags:
        - Tasks
      security:
        - oauth2:
            - tasks:read
      parameters:
        - $ref: "#/components/parameters/TaskId"
      responses:
        "200":
          description: Single task.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Task"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

    patch:
      operationId: updateTask
      summary: Update a task
      description: |
        Partially updates a task. Only the provided fields change. `label_ids`,
        `department_ids`, and `assignee_ids` replace the full set when present.
        Status changes fire automations exactly like the UI.
      tags:
        - Tasks
      security:
        - oauth2:
            - tasks:update
      parameters:
        - $ref: "#/components/parameters/TaskId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateTaskRequest"
      responses:
        "200":
          description: Updated task.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Task"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"

  /api/v1/tasks/{task}/comments:
    post:
      operationId: addTaskComment
      summary: Add a comment to a task
      description: Adds a markdown comment. Mentions written as `@[Name](user:uuid)` notify the mentioned users.
      tags:
        - Tasks
      security:
        - oauth2:
            - tasks:update
      parameters:
        - $ref: "#/components/parameters/TaskId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - body
              properties:
                body:
                  type: string
                  description: Comment body in markdown.
      responses:
        "201":
          description: Comment created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Comment"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"

  /api/v1/tasks/{task}/attachments:
    post:
      operationId: addTaskAttachment
      summary: Attach a file or link to a task
      description: |
        Two formats are accepted:

        - **File** — `multipart/form-data` with a `file` part (max 50 MB). `type` defaults to `file`.
        - **Link** — JSON body with `type: "url"`, a `url`, and an optional `title`.
      tags:
        - Tasks
      security:
        - oauth2:
            - tasks:update
      parameters:
        - $ref: "#/components/parameters/TaskId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: The file to upload (max 51200 KB).
                title:
                  type: string
          application/json:
            schema:
              type: object
              required:
                - type
                - url
              properties:
                type:
                  type: string
                  enum:
                    - url
                url:
                  type: string
                  format: uri
                  description: http(s) URL to attach.
                title:
                  type: string
      responses:
        "201":
          description: Attachment created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Attachment"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"

  /api/v1/tasks/{task}/checklists:
    post:
      operationId: addTaskChecklist
      summary: Add a checklist to a task
      tags:
        - Tasks
      security:
        - oauth2:
            - tasks:update
      parameters:
        - $ref: "#/components/parameters/TaskId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - items
              properties:
                name:
                  type: string
                  maxLength: 255
                items:
                  type: array
                  minItems: 1
                  items:
                    type: object
                    required:
                      - content
                    properties:
                      content:
                        type: string
                      is_completed:
                        type: boolean
      responses:
        "201":
          description: Checklist created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Checklist"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"

  /api/v1/tasks/{task}/time-entries:
    post:
      operationId: logTime
      summary: Log time against a task
      description: |
        Logs a completed time entry for the authenticated user. Provide either a
        `duration` string (`30m`, `2h`, `1h30m`, `3d`) or integer `duration_minutes`.
        Time cannot be logged on draft tasks.
      tags:
        - Time
      security:
        - oauth2:
            - time:write
      parameters:
        - $ref: "#/components/parameters/TaskId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                duration:
                  type: string
                  description: Duration string. Required unless duration_minutes is given.
                  example: 1h30m
                duration_minutes:
                  type: integer
                  minimum: 1
                  description: Duration in minutes. Required unless duration is given.
                date:
                  type: string
                  format: date
                  description: Entry date. Defaults to today.
                description:
                  type: string
                  maxLength: 1000
                  description: What the time was spent on.
      responses:
        "201":
          description: Time entry created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/TimeEntry"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"

  /api/v1/time-entries:
    get:
      operationId: listTimeEntries
      summary: List time entries
      description: |
        Lists time entries, defaulting to the authenticated user's own. Viewing
        another user's entries requires the `time_entries.view_others` permission.
        Running timers appear with `running: true` and no duration.
      tags:
        - Time
      security:
        - oauth2:
            - time:read
      parameters:
        - name: task_id
          in: query
          schema:
            type: integer
        - name: user_id
          in: query
          description: View another user's entries (requires permission).
          schema:
            type: integer
        - name: from
          in: query
          description: Only entries on or after this date.
          schema:
            type: string
            format: date
        - name: to
          in: query
          description: Only entries on or before this date.
          schema:
            type: string
            format: date
        - name: running_only
          in: query
          schema:
            type: boolean
      responses:
        "200":
          description: Paginated time entries.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedTimeEntries"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/timers/start:
    post:
      operationId: startTimer
      summary: Start a timer
      description: Starts a running timer on a task. A user can only have one running timer; drafts cannot be timed.
      tags:
        - Time
      security:
        - oauth2:
            - time:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - task_id
              properties:
                task_id:
                  type: integer
      responses:
        "201":
          description: Timer started.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/TimeEntry"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/ValidationError"

  /api/v1/timers/stop:
    post:
      operationId: stopTimer
      summary: Stop the running timer
      description: Stops the authenticated user's running timer and logs the elapsed time. Optionally override the duration, date, or description.
      tags:
        - Time
      security:
        - oauth2:
            - time:write
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                duration:
                  type: string
                  description: Override the logged duration, e.g. "45m".
                duration_minutes:
                  type: integer
                  minimum: 1
                date:
                  type: string
                  format: date
                description:
                  type: string
                  maxLength: 1000
      responses:
        "200":
          description: Timer stopped; the completed entry is returned.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/TimeEntry"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/ValidationError"

  /api/v1/retainers:
    get:
      operationId: listRetainers
      summary: List retainers
      description: |
        Lists retainers with budget health. Defaults to active and upcoming
        retainers; pass `include_completed=1` for ended ones. Requires the
        `retainers.view` permission.

        Tasks count toward a retainer when explicitly linked (see `retainer_id`
        on task creation and update) or when they belong to a project attached
        to the retainer. Time entries dated within the retainer period on those tasks
        consume the budget automatically.
      tags:
        - Reports
      security:
        - oauth2:
            - reports:read
      parameters:
        - name: client_id
          in: query
          schema:
            type: integer
        - name: include_completed
          in: query
          schema:
            type: boolean
      responses:
        "200":
          description: Paginated retainers.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedRetainers"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/retainers/{retainer}:
    get:
      operationId: getRetainer
      summary: Get a retainer with budget health
      tags:
        - Reports
      security:
        - oauth2:
            - reports:read
      parameters:
        - name: retainer
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Retainer with health detail.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Retainer"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/capacity:
    get:
      operationId: getCapacity
      summary: Get team capacity vs workload
      description: Returns workload versus capacity for active team members over the coming weeks. Requires the `capacity.view` permission.
      tags:
        - Reports
      security:
        - oauth2:
            - reports:read
      parameters:
        - name: department_id
          in: query
          schema:
            type: integer
        - name: weeks
          in: query
          description: Planning horizon in weeks (0.5–12). Defaults to 1.
          schema:
            type: number
      responses:
        "200":
          description: Capacity report.
          content:
            application/json:
              schema:
                type: object
                properties:
                  weeks:
                    type: number
                  department_id:
                    type:
                      - integer
                      - "null"
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/CapacityRow"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"

components:
  securitySchemes:
    oauth2:
      type: oauth2
      description: |
        OAuth2 Authorization Code Grant with PKCE, or a personal access token
        with the same scopes.
      flows:
        authorizationCode:
          authorizationUrl: http://localhost:8000/oauth/authorize
          tokenUrl: http://localhost:8000/oauth/token
          refreshUrl: http://localhost:8000/oauth/token/refresh
          scopes:
            user:read: View your profile information
            users:read: View team members
            clients:read: View clients
            clients:create: Create new clients
            projects:read: View projects
            projects:create: Create new projects
            tasks:read: View tasks
            tasks:create: Create new tasks
            tasks:update: Update existing tasks
            task-statuses:read: View task statuses
            departments:read: View departments
            labels:read: View labels
            time:read: View time entries
            time:write: Log time and manage timers
            reports:read: View retainer health and capacity reports

  parameters:
    TaskId:
      name: task
      in: path
      description: The task ID.
      required: true
      schema:
        type: integer

  schemas:
    User:
      type: object
      required:
        - uuid
        - name
        - email
        - avatar_url
        - timezone
        - created_at
      properties:
        id:
          type: integer
          description: Numeric identifier (present on the /users list; use for assignee_ids).
        uuid:
          type: string
          format: uuid
          description: Unique identifier for the user (use in @-mentions).
          example: 019d083f-77ef-7363-995f-50d908e11825
        name:
          type: string
          example: Jane Doe
        email:
          type: string
          format: email
          example: jane@example.com
        avatar_url:
          type:
            - string
            - "null"
          format: uri
        timezone:
          type: string
          example: Australia/Sydney
        suspended_at:
          type:
            - string
            - "null"
          format: date-time
        created_at:
          type: string
          format: date-time

    Client:
      type: object
      required:
        - id
        - name
        - task_prefix
      properties:
        id:
          type: integer
        name:
          type: string
          example: Acme Corp
        task_prefix:
          type: string
          example: ACM
        contact_name:
          type:
            - string
            - "null"
        contact_email:
          type:
            - string
            - "null"
        created_at:
          type: string
          format: date-time

    Project:
      type: object
      required:
        - id
        - name
        - description
        - task_prefix
        - client
        - created_at
      properties:
        id:
          type: integer
        name:
          type: string
          example: Website Redesign
        description:
          type:
            - string
            - "null"
        task_prefix:
          type:
            - string
            - "null"
          example: WEB
        client:
          $ref: "#/components/schemas/ClientSummary"
        created_at:
          type: string
          format: date-time

    Task:
      type: object
      required:
        - id
        - task_prefix
        - task_number
        - reference
        - title
        - description
        - priority
        - due_date
        - is_draft
        - created_at
        - completed_at
      properties:
        id:
          type: integer
          example: 42
        task_prefix:
          type: string
          example: PROJ
        task_number:
          type: integer
          example: 123
        reference:
          type: string
          description: >-
            Human-readable reference combining prefix and number. Subtasks
            carry a dotted child number (PROJ-123.2).
          example: PROJ-123
        is_parent:
          type: boolean
          description: True for a parent (container) task that groups subtasks.
        parent:
          type: object
          description: Present on subtasks; the parent task's identity.
          properties:
            reference:
              type: string
              example: PROJ-123
            title:
              type: string
        subtasks:
          type: array
          description: Present on parent tasks; the family's subtasks in order.
          items:
            type: object
            properties:
              reference:
                type: string
                example: PROJ-123.1
              title:
                type: string
        title:
          type: string
        description:
          type:
            - string
            - "null"
          description: Markdown description.
        priority:
          type:
            - string
            - "null"
          enum:
            - urgent
            - high
            - medium
            - low
            - lowest
            - null
        due_date:
          type:
            - string
            - "null"
          format: date
        is_draft:
          type: boolean
        job_number:
          type:
            - string
            - "null"
        estimate_minutes:
          type:
            - integer
            - "null"
        labels:
          type: array
          items:
            $ref: "#/components/schemas/IdName"
        departments:
          type: array
          items:
            $ref: "#/components/schemas/IdName"
        retainers:
          type: array
          description: Retainers this task's logged time counts toward.
          items:
            $ref: "#/components/schemas/IdName"
        status:
          $ref: "#/components/schemas/TaskStatus"
        project:
          oneOf:
            - $ref: "#/components/schemas/ProjectSummary"
            - type: "null"
        client:
          oneOf:
            - $ref: "#/components/schemas/ClientSummary"
            - type: "null"
        assignees:
          type: array
          items:
            $ref: "#/components/schemas/AssigneeSummary"
        owners:
          type: array
          description: The people tracking the work; owners approve estimates.
          items:
            $ref: "#/components/schemas/AssigneeSummary"
        created_at:
          type: string
          format: date-time
        completed_at:
          type:
            - string
            - "null"
          format: date-time
        meta:
          type: object
          properties:
            url:
              type: string
              format: uri
              description: Web URL for the task.
            created_by:
              $ref: "#/components/schemas/AssigneeSummary"

    TaskStatus:
      type: object
      required:
        - id
        - name
        - color
        - is_closed
      properties:
        id:
          type: integer
        name:
          type: string
          example: In Progress
        color:
          type: string
          example: "#3B82F6"
        is_closed:
          type: boolean

    TaskStatusFull:
      type: object
      required:
        - id
        - name
        - is_default
        - is_closed
      properties:
        id:
          type: integer
        name:
          type: string
        slug:
          type: string
        color:
          type: string
        category:
          type:
            - string
            - "null"
        sort_order:
          type: integer
        is_default:
          type: boolean
          description: New tasks land here when no task_status_id is given.
        is_closed:
          type: boolean

    Department:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
        name:
          type: string
        slug:
          type: string
        color:
          type:
            - string
            - "null"

    Label:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
        name:
          type: string
          example: Automated
        slug:
          type: string
        color:
          type:
            - string
            - "null"
        group:
          type:
            - string
            - "null"

    Comment:
      type: object
      required:
        - id
        - body
      properties:
        id:
          type: integer
        body:
          type: string
        is_internal:
          type: boolean
        user_id:
          type: integer
        created_at:
          type: string
          format: date-time

    Attachment:
      type: object
      required:
        - id
        - type
      properties:
        id:
          type: integer
        uuid:
          type: string
          format: uuid
        type:
          type: string
          enum:
            - file
            - url
        filename:
          type:
            - string
            - "null"
        url:
          type:
            - string
            - "null"
        title:
          type:
            - string
            - "null"
        mime_type:
          type:
            - string
            - "null"
        size_bytes:
          type:
            - integer
            - "null"
        created_at:
          type: string
          format: date-time

    Checklist:
      type: object
      required:
        - id
        - name
        - items
      properties:
        id:
          type: integer
        name:
          type: string
        items:
          type: array
          items:
            type: object
            properties:
              id:
                type: integer
              content:
                type: string
              is_completed:
                type: boolean

    TimeEntry:
      type: object
      required:
        - id
        - task_id
        - date
        - duration_minutes
        - running
      properties:
        id:
          type: integer
        task_id:
          type: integer
        task:
          type: object
          properties:
            id:
              type: integer
            reference:
              type: string
              example: PROJ-123
            title:
              type: string
        date:
          type:
            - string
            - "null"
          format: date
        duration_minutes:
          type:
            - integer
            - "null"
          description: Null while a timer is running.
        duration:
          type:
            - string
            - "null"
          description: Human-readable duration, e.g. "1h 30m".
        description:
          type:
            - string
            - "null"
        running:
          type: boolean
        started_at:
          type:
            - string
            - "null"
          format: date-time
          description: Set while a timer is running.
        created_at:
          type: string
          format: date-time

    Retainer:
      type: object
      required:
        - id
        - name
        - budget_hours
        - health
      properties:
        id:
          type: integer
        name:
          type: string
          example: Acme Monthly
        client:
          $ref: "#/components/schemas/ClientSummary"
        frequency:
          type: string
          example: monthly
        start_date:
          type: string
          format: date
        end_date:
          type: string
          format: date
        budget_hours:
          type: number
        used_minutes:
          type: integer
          description: Logged minutes within the retainer period.
        used:
          type: string
          description: Human-readable used time, e.g. "12h 30m".
        outstanding_estimated_minutes:
          type: integer
          description: Remaining estimated work on open retainer tasks.
        remaining_hours:
          type:
            - number
            - "null"
        unestimated_task_count:
          type: integer
        health:
          type: string
          enum:
            - unknown
            - on_track
            - ahead
            - at_risk
            - over_committed
            - over_budget
          description: Budget health status.
        health_label:
          type: string
        health_detail:
          type:
            - string
            - "null"

    CapacityRow:
      type: object
      required:
        - id
        - name
        - utilisation_percent
      properties:
        id:
          type: integer
        name:
          type: string
        capacity_minutes:
          type: integer
        capacity:
          type: string
          example: 38h
        workload_minutes:
          type: integer
        workload:
          type: string
          example: 42h 30m
        utilisation_percent:
          type: number

    ProjectSummary:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
        name:
          type: string

    ClientSummary:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
        name:
          type: string

    AssigneeSummary:
      type: object
      required:
        - uuid
        - name
      properties:
        uuid:
          type: string
          format: uuid
        name:
          type: string

    IdName:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
        name:
          type: string

    CreateTaskRequest:
      type: object
      required:
        - title
      properties:
        title:
          type: string
          maxLength: 255
          example: Implement dark mode
        description:
          type: string
          description: Markdown description. Mentions written as `@[Name](user:uuid)` notify users.
        project_id:
          type: integer
        client_id:
          type: integer
          description: Required unless `project_id` or `parent_task_id` is provided.
        parent_task_id:
          type: integer
          description: >-
            Create the task as a subtask of this parent task. The subtask
            inherits the parent's client, project, job number, and owners, so
            `client_id`, `project_id`, `job_number`, `is_draft`, and
            `owner_ids` must not be sent alongside it. `retainer_id` may be
            set; each subtask owns its retainer independently, and the parent
            holds none.
        task_status_id:
          type: integer
          description: Initial status ID (see /task-statuses). Defaults to the workspace default status.
        priority:
          type: string
          enum:
            - urgent
            - high
            - medium
            - low
            - lowest
        due_date:
          type: string
          format: date
        is_draft:
          type: boolean
          description: Create as a private draft (no automations, no time tracking).
        job_number:
          type: string
          maxLength: 50
        estimate_minutes:
          type: integer
          minimum: 1
        retainer_id:
          type: integer
          description: Retainer to count this task's logged time against. Requires the retainers.view permission.
        owner_ids:
          type: array
          minItems: 1
          items:
            type: integer
          description: >-
            User IDs to own the task (owners track the work and approve
            estimates). Defaults to the creator; passing this replaces that
            default. Ineligible users (suspended, or without access to
            private work) are skipped — if nobody remains, the creator stays
            as owner. Check the `owners` in the response.
        label_ids:
          type: array
          items:
            type: integer
          description: Label IDs (see /labels).
        assignee_ids:
          type: array
          items:
            type: integer
          description: User IDs (see /users).
        department_ids:
          type: array
          items:
            type: integer
          description: Department IDs (see /departments).
        screenshot:
          type: string
          description: Base64 data URL of an image to embed in the description.

    UpdateTaskRequest:
      type: object
      properties:
        title:
          type: string
          maxLength: 255
        description:
          type:
            - string
            - "null"
        task_status_id:
          type: integer
          description: New status ID (see /task-statuses).
        priority:
          type:
            - string
            - "null"
          enum:
            - urgent
            - high
            - medium
            - low
            - lowest
            - null
        due_date:
          type:
            - string
            - "null"
          format: date
        job_number:
          type:
            - string
            - "null"
          maxLength: 50
        estimate_minutes:
          type:
            - integer
            - "null"
          minimum: 1
        retainer_id:
          type:
            - integer
            - "null"
          description: >-
            Move the task to this retainer, or `null` to detach it (the task
            then falls back to its project's retainer unless excluded).
            Requires the retainers.view permission. Rejected for parent
            (container) tasks; each subtask owns its retainer independently.
        label_ids:
          type: array
          items:
            type: integer
          description: Replaces the task's full label set.
        assignee_ids:
          type: array
          items:
            type: integer
          description: Replaces the task's full assignee set.
        owner_ids:
          type: array
          minItems: 1
          items:
            type: integer
          description: >-
            Replaces the task's full owner set (owners track the work and
            approve estimates; a task must keep at least one). Owners are
            family-level: allowed on parent and standalone tasks, rejected
            for subtasks (they mirror the parent's owners). Setting a
            parent's owners updates every subtask.
        department_ids:
          type: array
          items:
            type: integer
          description: Replaces the task's full department set.

    ValidationError:
      type: object
      required:
        - message
        - errors
      properties:
        message:
          type: string
          example: The title field is required.
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
          example:
            title:
              - The title field is required.

    PaginationLinks:
      type: object
      properties:
        first:
          type:
            - string
            - "null"
          format: uri
        last:
          type:
            - string
            - "null"
          format: uri
        prev:
          type:
            - string
            - "null"
          format: uri
        next:
          type:
            - string
            - "null"
          format: uri

    PaginationMeta:
      type: object
      properties:
        current_page:
          type: integer
        from:
          type:
            - integer
            - "null"
        last_page:
          type: integer
        path:
          type: string
          format: uri
        per_page:
          type: integer
        to:
          type:
            - integer
            - "null"
        total:
          type: integer
        links:
          type: array
          items:
            type: object
            properties:
              url:
                type:
                  - string
                  - "null"
                format: uri
              label:
                type: string
              active:
                type: boolean

    PaginatedUsers:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/User"
        links:
          $ref: "#/components/schemas/PaginationLinks"
        meta:
          $ref: "#/components/schemas/PaginationMeta"

    PaginatedClients:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Client"
        links:
          $ref: "#/components/schemas/PaginationLinks"
        meta:
          $ref: "#/components/schemas/PaginationMeta"

    PaginatedProjects:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Project"
        links:
          $ref: "#/components/schemas/PaginationLinks"
        meta:
          $ref: "#/components/schemas/PaginationMeta"

    PaginatedTasks:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Task"
        links:
          $ref: "#/components/schemas/PaginationLinks"
        meta:
          $ref: "#/components/schemas/PaginationMeta"

    PaginatedTimeEntries:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/TimeEntry"
        links:
          $ref: "#/components/schemas/PaginationLinks"
        meta:
          $ref: "#/components/schemas/PaginationMeta"

    PaginatedRetainers:
      type: object
      required:
        - data
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Retainer"
        links:
          $ref: "#/components/schemas/PaginationLinks"
        meta:
          $ref: "#/components/schemas/PaginationMeta"

  responses:
    Unauthenticated:
      description: Authentication is required. The request did not include a valid access token.
      content:
        application/json:
          schema:
            type: object
            required:
              - message
            properties:
              message:
                type: string
                example: Unauthenticated.

    Forbidden:
      description: The access token does not have the required scope, or the user lacks the required permission.
      content:
        application/json:
          schema:
            type: object
            required:
              - message
            properties:
              message:
                type: string
                example: Invalid scope(s) provided.

    NotFound:
      description: The requested resource was not found.
      content:
        application/json:
          schema:
            type: object
            required:
              - message
            properties:
              message:
                type: string
                example: Not found.

    ValidationError:
      description: The request body failed validation.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ValidationError"
