openapi: 3.1.0

info:
  title: Office Recruiting Analytics API
  version: 1.0.0
  description: |
    API designed for AI-assisted querying of recruiting, applicants, employees,
    interviews, assignments, projects, onboarding, and engagement data.

    Each office must use its own unique API key. The API key identifies the office
    and all returned data is scoped to that office.

    Option endpoints can be fetched at the beginning of an AI session and cached
    temporarily. These endpoints help resolve human-readable names such as recruiter
    names, statuses, sources, roles, and job postings into IDs used by filters.

    ## Authentication
    Protected endpoints require the `x-api-key` header. Invalid or missing keys return `401`.

    ## Errors
    Error responses use a consistent JSON shape:
    ```json
    { "message": "...", "error_code": "BAD_REQUEST", "details": "..." }
    ```
    Common codes: `INVALID_API_KEY` (401), `OFFICE_NOT_FOUND` (404), `BAD_REQUEST` (400),
    `NOT_FOUND` (404), `INTERNAL_SERVER_ERROR` (500).

    ## Date filters
    - `target_start_date` and assignment/project `due_date` use date-only comparison (`YYYY-MM-DD`), without timezone conversion.
    - `created_date`, `interview_date`, and engagement `created_date` use the office timezone for datetime ranges.

servers:
  - url: https://data.atsmako.dev/api
    description: Development
  - url: https://data.atsmako.com/api
    description: Production

security:
  - ApiKeyAuth: []

tags:
  - name: Options
  - name: Applicants
  - name: Employees
  - name: Interviews
  - name: Jobs
  - name: Assignments
  - name: Projects
  - name: Engagement
  - name: Notes

paths:
  /users/recruiters:
    get:
      tags: [Options]
      summary: Get recruiter options
      description: Returns recruiters available for the office identified by the API key.
      responses:
        "200":
          description: Recruiter options
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/PersonOption"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /employees/interviewers:
    get:
      tags: [Options]
      summary: Get interviewer options
      responses:
        "200":
          description: Interviewer options
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/PersonOption"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /employees/leaders:
    get:
      tags: [Options]
      summary: Get leader options
      responses:
        "200":
          description: Leader options
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/PersonOption"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /applicants/statuses:
    get:
      tags: [Options]
      summary: Get applicant status options
      responses:
        "200":
          description: Applicant statuses
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/LabelOption"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /employees/statuses:
    get:
      tags: [Options]
      summary: Get employee status options
      responses:
        "200":
          description: Employee statuses
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/LabelOption"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /applicants/sub-statuses:
    get:
      tags: [Options]
      summary: Get applicant sub-status options
      responses:
        "200":
          description: Applicant sub-statuses
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/LabelOption"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /applications/sources:
    get:
      tags: [Options]
      summary: Get application source options
      responses:
        "200":
          description: Application sources
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/LabelOption"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /applications/next-actions:
    get:
      tags: [Options]
      summary: Get next action options
      responses:
        "200":
          description: Next actions
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/LabelOption"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /states:
    get:
      tags: [Options]
      summary: Get states grouped by country
      description: |
        Public endpoint. Does not require an API key.

        Returns all states and provinces grouped by ISO 3166-1 alpha-2 country code.
        Each top-level key is a country code (for example, `US`, `CA`) and its value is
        the list of states for that country, sorted alphabetically by name.
      security: []
      responses:
        "200":
          description: States grouped by country ISO code
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatesByCountry"
              example:
                US:
                  - id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    name: "California"
                    code: "CA"
                  - id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
                    name: "Texas"
                    code: "TX"
                CA:
                  - id: "c3d4e5f6-a7b8-9012-cdef-123456789012"
                    name: "Ontario"
                    code: "ON"
                  - id: "d4e5f6a7-b8c9-0123-def0-234567890123"
                    name: "Quebec"
                    code: "QC"
        "500":
          $ref: "#/components/responses/ServerError"

  /jobs/roles:
    get:
      tags: [Options]
      summary: Get role options
      responses:
        "200":
          description: Role options
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/LabelOption"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /jobs/job-postings:
    get:
      tags: [Options]
      summary: Get job posting options
      responses:
        "200":
          description: Job posting options
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/JobPostingOption"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /interviews/statuses:
    get:
      tags: [Options]
      summary: Get interview status options
      responses:
        "200":
          description: Interview statuses
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/LabelOption"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /applicants/count:
    post:
      tags: [Applicants]
      summary: Count applicants
      description: |
        Counts applicants matching the provided filters. When `status_ids` is omitted,
        applicants with status `DUPLICATE` or `BLOCKED` are excluded by default.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ApplicantFilters"
      responses:
        "200":
          $ref: "#/components/responses/CountResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /applicants/search:
    post:
      tags: [Applicants]
      summary: Search applicants
      description: |
        Returns a paginated list of applicants matching the provided filters and optional `search_param`.
        When searching by phone, send digits only (e.g. `"6153366209"`) — no country code, spaces, dashes, or parentheses.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SearchApplicantsRequest"
            example:
              pagination:
                limit: 100
                page: 1
              sort:
                field: name
                order: 1
              search_param: "6153366209"
              filters:
                status_ids: ["fb2a566b-9f7c-4dd0-9cf9-ae1f9fb1892b"]
                qualified: true
      responses:
        "200":
          description: Paginated applicants
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SearchApplicantsResponse"
              example:
                total: 1
                page: 1
                limit: 100
                results:
                  - id: "fa23d29c-655d-4618-b7e6-f0a87bcd4309"
                    name: "John Doe"
                    status: "Applicant"
                    sub_status: "Loop 1"
                    email_address: "test@gmail.com"
                    phone: "1234567890"
                    source: "Indeed"
                    target_start_date: "2026-07-01"
                    qualified: true
                    spoke_to: false
                    viewed: true
                    role: "Sales"
                    next_action: "Review Resumes"
                    recruited_by: "ATS Development"
                    job_posting: "BE developer"
                    assigned_leader: "Oliver Test"
                    created_datetime: "2026-01-12T19:15:34.000Z"
                    latest_interview_datetime: null
                    latest_interview_duration_min: null
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /employees/count:
    post:
      tags: [Employees]
      summary: Count employees
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmployeeFilters"
      responses:
        "200":
          $ref: "#/components/responses/CountResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /employees/search:
    post:
      tags: [Employees]
      summary: Search employees
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SearchEmployeesRequest"
      responses:
        "200":
          description: Paginated employees
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SearchEmployeesResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /interviews/count:
    post:
      tags: [Interviews]
      summary: Count interviews
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InterviewFilters"
      responses:
        "200":
          $ref: "#/components/responses/CountResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /interviews/search:
    post:
      tags: [Interviews]
      summary: Search interviews
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InterviewFilters"
      responses:
        "200":
          description: Interviews
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: "#/components/schemas/Interview"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /jobs/job-postings/count:
    post:
      tags: [Jobs]
      summary: Count job postings
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/JobPostingFilters"
      responses:
        "200":
          $ref: "#/components/responses/CountResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /jobs/job-postings/search:
    post:
      tags: [Jobs]
      summary: Search job postings
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/JobPostingFilters"
      responses:
        "200":
          description: Job postings
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: "#/components/schemas/JobPosting"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /assignments/count:
    post:
      tags: [Assignments]
      summary: Count assignments
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AssignmentFilters"
      responses:
        "200":
          $ref: "#/components/responses/CountResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /assignments/search:
    post:
      tags: [Assignments]
      summary: Search assignments
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AssignmentFilters"
      responses:
        "200":
          description: Assignments
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: "#/components/schemas/TaskItem"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /projects/count:
    post:
      tags: [Projects]
      summary: Count projects
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaskFilters"
      responses:
        "200":
          $ref: "#/components/responses/CountResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /projects/search:
    post:
      tags: [Projects]
      summary: Search projects
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaskFilters"
      responses:
        "200":
          description: Projects
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: "#/components/schemas/TaskItem"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /employees/onboarding/count:
    post:
      tags: [Employees]
      summary: Count employees in onboarding
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OnboardingFilters"
      responses:
        "200":
          $ref: "#/components/responses/CountResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /employees/onboarding/search:
    post:
      tags: [Employees]
      summary: Search employees in onboarding
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OnboardingFilters"
      responses:
        "200":
          description: Employees in onboarding
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: "#/components/schemas/OnboardingEmployee"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /engagement/sms/count:
    post:
      tags: [Engagement]
      summary: Count SMS messages
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SmsCountFilters"
      responses:
        "200":
          $ref: "#/components/responses/CountResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /engagement/emails/count:
    post:
      tags: [Engagement]
      summary: Count emails
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmailCountFilters"
      responses:
        "200":
          $ref: "#/components/responses/CountResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /engagement/sms/by-application:
    get:
      tags: [Engagement]
      summary: Get SMS grouped by application
      description: |
        Returns SMS engagements grouped by application. Provide either `applicationId` or
        `searchQuery` (partial match on applicant name, phone, or email). When both are sent,
        `applicationId` takes precedence.
      parameters:
        - $ref: "#/components/parameters/ApplicationIdQuery"
        - $ref: "#/components/parameters/ApplicationSearchQuery"
      responses:
        "200":
          description: SMS grouped by application
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ApplicationSmsGroup"
              example:
                - application_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  applicant_name: "Jane Doe"
                  sms:
                    - id: "sms-uuid-1"
                      direction: outbound
                      read: true
                      sent_to: "+15551234567"
                      sent_from: "+15559876543"
                      content: "Reminder for your interview"
                      created_datetime: "2026-06-15T14:30:00.000Z"
                      sent_by: "John Recruiter"
                      sent_automatically: false
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /engagement/emails/by-application:
    get:
      tags: [Engagement]
      summary: Get emails grouped by application
      description: |
        Returns email engagements grouped by application. Provide either `applicationId` or
        `searchQuery` (partial match on applicant name, phone, or email). When both are sent,
        `applicationId` takes precedence.
      parameters:
        - $ref: "#/components/parameters/ApplicationIdQuery"
        - $ref: "#/components/parameters/ApplicationSearchQuery"
      responses:
        "200":
          description: Emails grouped by application
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ApplicationEmailGroup"
              example:
                - application_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  applicant_name: "Jane Doe"
                  emails:
                    - id: "email-uuid-1"
                      subject: "Welcome"
                      body: "Thanks for applying"
                      created_datetime: "2026-06-15T14:30:00.000Z"
                      sent_by: "John Recruiter"
                      sent_automatically: false
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /notes/by-application:
    get:
      tags: [Notes]
      summary: Get notes grouped by application
      description: |
        Returns application notes grouped by application. Provide either `applicationId` or
        `searchQuery` (partial match on applicant name, phone, or email). When both are sent,
        `applicationId` takes precedence.
      parameters:
        - $ref: "#/components/parameters/ApplicationIdQuery"
        - $ref: "#/components/parameters/ApplicationSearchQuery"
      responses:
        "200":
          description: Notes grouped by application
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ApplicationNoteGroup"
              example:
                - application_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  applicant_name: "Jane Doe"
                  notes:
                    - id: "note-uuid-1"
                      content: "Followed up by phone"
                      created_datetime: "2026-06-15T14:30:00.000Z"
                      created_by: "John Recruiter"
                      automated: false
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/OfficeNotFound"
        "500":
          $ref: "#/components/responses/ServerError"

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: Unique API key for the office. All protected endpoints return data scoped to the office identified by this key.

  parameters:
    ApplicationIdQuery:
      name: applicationId
      in: query
      required: false
      schema:
        type: string
        format: uuid
      description: Application UUID. Takes precedence over `searchQuery` when both are provided.

    ApplicationSearchQuery:
      name: searchQuery
      in: query
      required: false
      schema:
        type: string
      description: Partial match on applicant name, phone number, or email address.

  responses:
    CountResponse:
      description: Total count
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/CountResult"

    ErrorResponse:
      description: Standard error payload
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

    BadRequest:
      description: Invalid request parameters or missing required query params
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            message: Either applicationId or searchQuery must be provided
            error_code: BAD_REQUEST

    ValidationError:
      description: Request body failed Joi validation
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            message: Request body validation error.
            error_code: BAD_REQUEST
            details: '"interview_date" is required'

    Unauthorized:
      description: Invalid or missing API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            message: Invalid API key.
            error_code: INVALID_API_KEY

    OfficeNotFound:
      description: Office not found for the provided API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            message: Office not found.
            error_code: OFFICE_NOT_FOUND

    ServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            message: Internal server error.
            error_code: INTERNAL_SERVER_ERROR

  schemas:
    ErrorResponse:
      type: object
      required: [message, error_code]
      properties:
        message:
          type: string
        error_code:
          type: string
          enum: [BAD_REQUEST, INVALID_API_KEY, OFFICE_NOT_FOUND, NOT_FOUND, INTERNAL_SERVER_ERROR]
        details:
          type: string
          nullable: true

    CountResult:
      type: object
      required: [total]
      properties:
        total:
          type: integer
          example: 42

    DateFilter:
      type: object
      description: |
        Inclusive date range. For date-only fields (`target_start_date`, `due_date`) values are
        compared as `YYYY-MM-DD` without timezone conversion. For datetime fields, the office
        timezone is applied.
      properties:
        from:
          type: string
          format: date
          nullable: true
          example: "2026-06-01"
        to:
          type: string
          format: date
          nullable: true
          example: "2026-06-30"

    PersonOption:
      type: object
      required: [id, full_name]
      properties:
        id:
          type: string
        full_name:
          type: string

    LabelOption:
      type: object
      required: [id, label]
      properties:
        id:
          type: string
        label:
          type: string

    JobPostingOption:
      type: object
      required: [id, position, status]
      properties:
        id:
          type: string
        position:
          type: string
        status:
          type: string
          enum: [open, closed]

    StateItem:
      type: object
      required: [id, name, code]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          example: California
        code:
          type: string
          description: State or province code.
          example: CA

    StatesByCountry:
      type: object
      description: |
        Map of ISO 3166-1 alpha-2 country codes to their state/province lists.
        Keys are uppercase country codes (for example, `US`, `CA`).
      additionalProperties:
        type: array
        items:
          $ref: "#/components/schemas/StateItem"
      example:
        US:
          - id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
            name: "California"
            code: "CA"
        CA:
          - id: "c3d4e5f6-a7b8-9012-cdef-123456789012"
            name: "Ontario"
            code: "ON"

    Pagination:
      type: object
      properties:
        limit:
          type: integer
          nullable: true
          default: 100
          description: Defaults to 100 when omitted or null.
        page:
          type: integer
          minimum: 1
          default: 1

    SortOrder:
      type: integer
      enum: [1, -1, 0]
      description: 1 for ascending, -1 for descending, 0 for no sorting.

    ApplicantFilters:
      type: object
      properties:
        recruited_by_ids:
          type: array
          nullable: true
          items:
            type: string
        next_action_ids:
          type: array
          nullable: true
          items:
            type: string
        status_ids:
          type: array
          nullable: true
          items:
            type: string
        sub_status_ids:
          type: array
          nullable: true
          items:
            type: string
        source_ids:
          type: array
          nullable: true
          items:
            type: string
        state_ids:
          type: array
          nullable: true
          items:
            type: string
        role_ids:
          type: array
          nullable: true
          items:
            type: string
        job_posting_ids:
          type: array
          nullable: true
          items:
            type: string
        assigned_leader_ids:
          type: array
          nullable: true
          items:
            type: string
        target_start_date:
          $ref: "#/components/schemas/DateFilter"
          description: Date-only filter (no timezone conversion).
        created_date:
          $ref: "#/components/schemas/DateFilter"
        qualified:
          type: boolean
          nullable: true
        spoke_to:
          type: boolean
          nullable: true
        viewed:
          type: boolean
          nullable: true

    EmployeeFilters:
      type: object
      properties:
        recruited_by_ids:
          type: array
          nullable: true
          items:
            type: string
        next_action_ids:
          type: array
          nullable: true
          items:
            type: string
        status_ids:
          type: array
          nullable: true
          items:
            type: string
        source_ids:
          type: array
          nullable: true
          items:
            type: string
        state_ids:
          type: array
          nullable: true
          items:
            type: string
        role_ids:
          type: array
          nullable: true
          items:
            type: string
        job_posting_ids:
          type: array
          nullable: true
          items:
            type: string
        assigned_leader_ids:
          type: array
          nullable: true
          items:
            type: string
        target_start_date:
          $ref: "#/components/schemas/DateFilter"
          description: Date-only filter (no timezone conversion).
        created_date:
          $ref: "#/components/schemas/DateFilter"
        is_leader:
          type: boolean
          nullable: true

    SearchApplicantsRequest:
      type: object
      properties:
        pagination:
          $ref: "#/components/schemas/Pagination"
        sort:
          type: object
          nullable: true
          properties:
            field:
              type: string
              nullable: true
              enum: [name, created_date, status, sub_status]
            order:
              $ref: "#/components/schemas/SortOrder"
        search_param:
          type: string
          nullable: true
          description: |
            Partial match on applicant name, phone, or email.
            For phone searches use digits only as stored in the database (e.g. `"1234567890"`).
            Do not include country code (`+1`), spaces, dashes, or parentheses.
          example: "1234567890"
        filters:
          $ref: "#/components/schemas/ApplicantFilters"

    SearchEmployeesRequest:
      type: object
      properties:
        pagination:
          $ref: "#/components/schemas/Pagination"
        sort:
          type: object
          nullable: true
          properties:
            field:
              type: string
              nullable: true
              enum: [name, created_date, status]
            order:
              $ref: "#/components/schemas/SortOrder"
        search_param:
          type: string
          nullable: true
          description: |
            Partial match on employee name, phone, or email.
            For phone searches use digits only as stored in the database (e.g. `"1234567890"`).
            Do not include country code (`+1`), spaces, dashes, or parentheses.
          example: "1234567890"
        filters:
          $ref: "#/components/schemas/EmployeeFilters"

    SearchApplicantsResponse:
      type: object
      properties:
        total:
          type: integer
        page:
          type: integer
        limit:
          type: integer
        results:
          type: array
          items:
            $ref: "#/components/schemas/Applicant"

    SearchEmployeesResponse:
      type: object
      properties:
        total:
          type: integer
        page:
          type: integer
        limit:
          type: integer
        results:
          type: array
          items:
            $ref: "#/components/schemas/Employee"

    Applicant:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        status:
          type: string
        sub_status:
          type: string
          nullable: true
        email_address:
          type: string
          nullable: true
        phone:
          type: string
          nullable: true
          description: 10-digit phone number without formatting (e.g. "1234567890").
          example: "1234567890"
        source:
          type: string
          nullable: true
        target_start_date:
          type: string
          format: date
          nullable: true
        qualified:
          type: boolean
        spoke_to:
          type: boolean
        viewed:
          type: boolean
        role:
          type: string
          nullable: true
        next_action:
          type: string
          nullable: true
        recruited_by:
          type: string
        job_posting:
          type: string
          nullable: true
        assigned_leader:
          type: string
          nullable: true
        created_datetime:
          type: string
          format: date-time
          nullable: true
        latest_interview_datetime:
          type: string
          format: date-time
          nullable: true
        latest_interview_duration_min:
          type: integer
          nullable: true

    Employee:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        status:
          type: string
        email_address:
          type: string
          nullable: true
        phone:
          type: string
          nullable: true
        source:
          type: string
          nullable: true
        target_start_date:
          type: string
          format: date
          nullable: true
        role:
          type: string
          nullable: true
        employee_id:
          type: string
          nullable: true
        recruited_by:
          type: string

    InterviewFilters:
      type: object
      properties:
        interview_status_ids:
          type: array
          nullable: true
          description: Interview status IDs from `GET /interviews/statuses` (e.g. scheduled, completed). Not applicant sub-statuses.
          items:
            type: string
            format: uuid
        interviewer_ids:
          type: array
          nullable: true
          items:
            type: string
        booked_by_ids:
          type: array
          nullable: true
          description: Recruiter IDs of the users who booked the interviews.
          items:
            type: string
        interview_date:
          $ref: "#/components/schemas/DateFilter"
        is_first_interview:
          type: boolean
          nullable: true
          description: |
            When set, filters interviews whose stage is a first-interview applicant sub-status for the office.
            Takes precedence over `interview_stage_ids`. Resolve sub-status IDs via `GET /applicants/sub-statuses`.
        is_second_interview:
          type: boolean
          nullable: true
          description: |
            When set, filters interviews whose stage is a second-interview applicant sub-status for the office.
            Takes precedence over `interview_stage_ids`. Resolve sub-status IDs via `GET /applicants/sub-statuses`.
        interview_stage_ids:
          type: array
          nullable: true
          description: |
            Applicant sub-status IDs (`GET /applicants/sub-statuses`), not interview status IDs
            (`GET /interviews/statuses`). Filters `application_interview.interview_stage`.
            Ignored when `is_first_interview` or `is_second_interview` is set.
          items:
            type: string
            format: uuid

    SearchInterviewsRequest:
      deprecated: true
      description: Deprecated. Use `InterviewFilters` directly as the request body for `/interviews/search`.
      type: object
      properties:
        range:
          $ref: "#/components/schemas/DateFilter"
        filters:
          $ref: "#/components/schemas/InterviewFilters"

    Interview:
      type: object
      properties:
        id:
          type: string
        applicant_name:
          type: string
        datetime:
          type: string
          format: date-time
        stage:
          type: string
          description: Applicant sub-status label at the time of the interview (`GET /applicants/sub-statuses`).
        duration_min:
          type: integer
        status:
          type: string
        booked_by:
          type: string
        interviewer:
          type: string

    JobPostingFilters:
      type: object
      properties:
        source_ids:
          type: array
          nullable: true
          items:
            type: string
        sub_status_ids:
          type: array
          nullable: true
          items:
            type: string
        role_ids:
          type: array
          nullable: true
          items:
            type: string
        status:
          type: string
          nullable: true
          enum: [open, closed]

    JobPosting:
      type: object
      properties:
        id:
          type: string
        position:
          type: string
        role:
          type: string
          nullable: true
        description:
          type: string
        status:
          type: string
        questions:
          type: array
          items:
            type: string
        source:
          type: string
          nullable: true
        sub_status:
          type: string
          nullable: true
        opened_datetime:
          type: string
          format: date-time
        closed_datetime:
          type: string
          format: date-time
          nullable: true

    TaskStatus:
      type: string
      enum: [ASSIGNED, CLOSED, IN_PROGRESS, OPEN]

    Priority:
      type: string
      enum: [LOW, MEDIUM, HIGH]

    TaskFilters:
      type: object
      properties:
        due_date:
          $ref: "#/components/schemas/DateFilter"
          description: Date-only filter (no timezone conversion).
        status:
          $ref: "#/components/schemas/TaskStatus"
        priority:
          $ref: "#/components/schemas/Priority"
        assigned_to_ids:
          type: array
          nullable: true
          items:
            type: string
        created_by_ids:
          type: array
          nullable: true
          items:
            type: string

    AssignmentFilters:
      allOf:
        - $ref: "#/components/schemas/TaskFilters"
        - type: object
          properties:
            project_ids:
              type: array
              nullable: true
              items:
                type: string
              description: Filter assignments by parent project IDs.

    TaskItem:
      type: object
      properties:
        id:
          type: string
        subject:
          type: string
        due_date:
          type: string
          format: date
        status:
          type: string
        priority:
          type: string
        assigned_to:
          type: string

    OfferStatus:
      type: string
      enum: [PENDING, ACCEPTED, REJECTED, EXPIRED, EXTENDED]
      description: Derived from the latest job offer for the application.

    OnboardingFilters:
      type: object
      properties:
        recruited_by_ids:
          type: array
          nullable: true
          items:
            type: string
        offer_statuses:
          type: array
          nullable: true
          items:
            $ref: "#/components/schemas/OfferStatus"
        orientation_attended:
          type: boolean
          nullable: true
        onboarding_confirmed:
          type: boolean
          nullable: true
        onboarding_steps_completed:
          type: boolean
          nullable: true
          description: When true, all active onboarding steps are completed; when false, at least one step is incomplete.
        target_start_date:
          $ref: "#/components/schemas/DateFilter"
          description: Date-only filter (no timezone conversion).

    LatestJobOffer:
      type: object
      nullable: true
      properties:
        accepted_datetime:
          type: string
          format: date-time
          nullable: true
        declined_datetime:
          type: string
          format: date-time
          nullable: true
        offer_deadline:
          type: string
          format: date
          nullable: true
        created_datetime:
          type: string
          format: date-time
          nullable: true
        version:
          type: integer
          nullable: true

    OnboardingEmployee:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        orientation_attended_datetime:
          type: string
          format: date-time
          nullable: true
        onboarding_confirmed_datetime:
          type: string
          format: date-time
          nullable: true
        onboarding_steps_completed_percentage:
          type: integer
          nullable: true
          description: Percentage of active onboarding steps completed (0–100). Null when no active steps exist.
        latest_job_offer:
          $ref: "#/components/schemas/LatestJobOffer"
        target_start_date:
          type: string
          format: date
          nullable: true

    SmsCountFilters:
      type: object
      properties:
        sent_by_recruiter_ids:
          type: array
          nullable: true
          items:
            type: string
        direction:
          type: string
          nullable: true
          enum: [inbound, outbound]
        read:
          type: boolean
          nullable: true
        created_date:
          $ref: "#/components/schemas/DateFilter"

    EmailCountFilters:
      type: object
      properties:
        sent_by_recruiter_ids:
          type: array
          nullable: true
          items:
            type: string
        created_date:
          $ref: "#/components/schemas/DateFilter"

    SmsItem:
      type: object
      properties:
        id:
          type: string
        direction:
          type: string
          enum: [inbound, outbound]
        read:
          type: boolean
        sent_to:
          type: string
          nullable: true
        sent_from:
          type: string
          nullable: true
        content:
          type: string
          nullable: true
        created_datetime:
          type: string
          format: date-time
        sent_by:
          type: string
          nullable: true
        sent_automatically:
          type: boolean

    EmailItem:
      type: object
      properties:
        id:
          type: string
        subject:
          type: string
          nullable: true
        body:
          type: string
          nullable: true
        created_datetime:
          type: string
          format: date-time
        sent_by:
          type: string
        sent_automatically:
          type: boolean

    NoteItem:
      type: object
      properties:
        id:
          type: string
        content:
          type: string
        created_datetime:
          type: string
          format: date-time
          nullable: true
        created_by:
          type: string
        automated:
          type: boolean

    ApplicationSmsGroup:
      type: object
      required: [application_id, applicant_name, sms]
      properties:
        application_id:
          type: string
          format: uuid
        applicant_name:
          type: string
        sms:
          type: array
          items:
            $ref: "#/components/schemas/SmsItem"

    ApplicationEmailGroup:
      type: object
      required: [application_id, applicant_name, emails]
      properties:
        application_id:
          type: string
          format: uuid
        applicant_name:
          type: string
        emails:
          type: array
          items:
            $ref: "#/components/schemas/EmailItem"

    ApplicationNoteGroup:
      type: object
      required: [application_id, applicant_name, notes]
      properties:
        application_id:
          type: string
          format: uuid
        applicant_name:
          type: string
        notes:
          type: array
          items:
            $ref: "#/components/schemas/NoteItem"
