---
openapi: "3.0.1"
info:
  title: "Timebutler API v2"
  description: "REST API for the Timebutler HR SaaS. Used by integrators for absence,\
    \ time tracking, substitute, and user profile workflows. All endpoints except\
    \ /oauth2/token require a bearer JWT access token."
  contact:
    name: "Timebutler Support"
    url: "https://www.timebutler.com"
    email: "support@timebutler.com"
  license:
    name: "Proprietary"
  version: "1.0"
  x-logo:
    url: "/images/logo/logo.png"
    altText: "Timebutler"
servers:
- url: "https://app.timebutler.com/api/v2"
- url: "/api/v2"
tags:
- name: "Absence requests"
  description: "Vacation, sick-leave and other time-off requests. Employees submit\
    \ and manage their own requests (`/my`, `POST`, `PUT /{id}`, `DELETE /{id}`);\
    \ managers and admins see their team's open requests (`/team`, `/pending-count`)\
    \ and approve or reject them (`POST /{id}/approve`, `POST /{id}/reject`). All\
    \ endpoints require a bearer token."
- name: "Absence types"
  description: "Lookup list of absence types configured for the company (vacation,\
    \ sick leave, etc.). Used to populate dropdowns when creating an absence request."
- name: "Cancellation requests"
  description: "Requests by employees to cancel (delete) an already-approved absence.\
    \ Managers and admins see pending cancellation requests for their team and can\
    \ approve or reject them."
- name: "Categories"
  description: "Lookup list of time-tracking categories for the company. Used when\
    \ creating or stopping a clock entry."
- name: "Authentication"
  description: "OAuth 2.0 token endpoint. Issues JWT access tokens and rotating refresh\
    \ tokens. This endpoint is public — no bearer token is required."
- name: "Projects"
  description: "Lookup list of time-tracking projects for the company. Favorites are\
    \ listed first. Used when creating a clock entry or time-tracking record."
- name: "Substitute employees"
  description: "Lookup list of employees that may be selected as a substitute when\
    \ submitting an absence request."
- name: "Substitute requests"
  description: "Requests sent to the authenticated user asking them to act as a substitute\
    \ during a colleague's absence. The user can accept or reject each request."
- name: "Time clock"
  description: "Real-time time clock. Start, pause, resume, stop, or cancel the running\
    \ clock for the authenticated user. Business-trip mode is also controlled here."
- name: "Time entry requests"
  description: "Pending change or deletion requests for time entries that require\
    \ manager or admin approval. Managers and admins can list, approve, and reject\
    \ these requests."
- name: "Time entries"
  description: "Manage individual time-tracking entries. Managers and admins can see\
    \ how many entries are pending approval; any user can delete their own entries\
    \ (subject to month-lock and approval-workflow rules)."
- name: "Time tracking"
  description: "Today's work and time-off summaries for the authenticated user, plus\
    \ an endpoint for submitting completed clock entries (start/end timestamps) directly\
    \ without the real-time time clock."
- name: "User profile"
  description: "Basic profile information for the authenticated user: name, email,\
    \ role, department, and avatar URL."
paths:
  /absence-requests/{id}/approve:
    post:
      tags:
      - "Absence requests"
      summary: "Approve a pending absence request"
      description: "Records the caller's approval on a request in the `pending` state.\
        \ If the employee has multiple managers, the request only transitions to `approved`\
        \ once all managers have approved (or if an admin approves). Callers must\
        \ be an admin or the direct manager of the request's owner."
      operationId: "approve"
      parameters:
      - name: "id"
        in: "path"
        description: "Internal ID of the absence request to approve."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 12345
      responses:
        "200":
          description: "Request after the approval has been recorded."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AbsenceRequestDto"
        "401":
          description: "Missing or invalid bearer token."
          content:
            application/json: {}
        "403":
          description: "Caller is not a manager or admin, or is not permitted to act\
            \ on this particular request."
          content:
            application/json: {}
        "404":
          description: "No request with the given `id` exists for the caller's company."
          content:
            application/json: {}
        "409":
          description: "Request is not in `pending` state (already approved, rejected,\
            \ or deleted)."
          content:
            application/json: {}
        "500":
          description: "Unexpected server error while updating the request."
          content:
            application/json: {}
      security:
      - bearerAuth: []
  /absence-requests:
    get:
      tags:
      - "Absence requests"
      summary: "List the caller's own absence requests (alias)"
      description: "Convenience alias that returns the same result as `GET /my`. Prefer\
        \ `/my` in new integrations — this path exists for backwards compatibility."
      operationId: "getRequests"
      responses:
        "200":
          description: "List of the caller's requests. Empty array if there are none."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/AbsenceRequestDto"
        "401":
          description: "Missing or invalid bearer token."
          content:
            application/json: {}
        "403":
          description: "Caller's account is inactive."
          content:
            application/json: {}
        "500":
          description: "Unexpected server error."
          content:
            application/json: {}
      security:
      - bearerAuth: []
    post:
      tags:
      - "Absence requests"
      summary: "Create a new absence request"
      description: "Submits a new absence or vacation request for the caller. The\
        \ resulting status depends on the absence type's configuration: types that\
        \ require manager approval start as `pending`, otherwise they go straight\
        \ to `approved`. Dates are ISO-8601 (`YYYY-MM-DD`). For half-day requests\
        \ set `halfDay` to `morning` or `afternoon` and use the same value for `startDate`\
        \ and `endDate`."
      operationId: "create"
      requestBody:
        description: "Fields of the new absence request."
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAbsenceRequest"
        required: true
      responses:
        "200":
          description: "Created request, in the state the server assigned."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AbsenceRequestDto"
        "400":
          description: "Missing, malformed or mutually inconsistent fields (invalid\
            \ `absenceTypeId`, unparseable date, `endDate < startDate`, bad `halfDay`\
            \ value, or unknown `substituteEmployeeId`)."
          content:
            application/json: {}
        "401":
          description: "Missing or invalid bearer token."
          content:
            application/json: {}
        "403":
          description: "Caller's role is not allowed to enter this absence type."
          content:
            application/json: {}
        "500":
          description: "Unexpected server error while persisting the request."
          content:
            application/json: {}
      security:
      - bearerAuth: []
  /absence-requests/{id}:
    put:
      tags:
      - "Absence requests"
      summary: "Update an existing absence request"
      description: "Updates a request that is still in the `pending` or `edit` state.\
        \ Only the fields provided in the request body are changed; `null` fields\
        \ are left as they were. Non-admins can only update their own requests. Requests\
        \ that have already been approved or rejected cannot be updated and will return\
        \ `409 Conflict`."
      operationId: "update"
      parameters:
      - name: "id"
        in: "path"
        description: "Internal ID of the absence request to update. Matches `id` from\
          \ `GET /my` or `GET /team`."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 12345
      requestBody:
        description: "Fields to update. Omit a field (send `null`) to keep the existing\
          \ value."
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateAbsenceRequest"
        required: true
      responses:
        "200":
          description: "Updated request as stored."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AbsenceRequestDto"
        "400":
          description: "Malformed body or invalid field combination."
          content:
            application/json: {}
        "401":
          description: "Missing or invalid bearer token."
          content:
            application/json: {}
        "403":
          description: "Caller is not the owner of this request and not an admin,\
            \ or the absence type is forbidden for the caller's role."
          content:
            application/json: {}
        "404":
          description: "No request with the given `id` exists for the caller's company."
          content:
            application/json: {}
        "409":
          description: "Request is no longer in a state that permits editing (already\
            \ approved, rejected, or deleted)."
          content:
            application/json: {}
        "500":
          description: "Unexpected server error while persisting the update."
          content:
            application/json: {}
      security:
      - bearerAuth: []
    delete:
      tags:
      - "Absence requests"
      summary: "Delete an absence request or send a cancellation request"
      description: "Behaviour depends on the current state of the request and the\
        \ caller's rights: if the caller may delete the request outright it is removed\
        \ and the `result` field of the response is `\"deleted\"`. Otherwise a cancellation\
        \ request is created that the employee's managers then have to approve, and\
        \ the response `result` is `\"cancellation_requested\"`. A reason may be required\
        \ by the company's configuration when only cancellation is possible."
      operationId: "delete"
      parameters:
      - name: "id"
        in: "path"
        description: "Internal ID of the absence request to delete or cancel."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 12345
      requestBody:
        description: "Optional body with a free-text `reason`. Required for cancellation\
          \ requests when the company's configuration forces a reason."
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DeleteAbsenceRequest"
      responses:
        "200":
          description: "Outcome of the delete/cancel attempt. The `result` field tells\
            \ the caller which path was taken."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteResultResponse"
        "400":
          description: "Cancellation reason missing where the company requires one."
          content:
            application/json: {}
        "401":
          description: "Missing or invalid bearer token."
          content:
            application/json: {}
        "403":
          description: "Caller may neither delete nor cancel this request (wrong company,\
            \ or permissions on the request's state do not allow any action)."
          content:
            application/json: {}
        "404":
          description: "No request with the given `id` exists for the caller's company."
          content:
            application/json: {}
        "500":
          description: "Unexpected server error while deleting or cancelling."
          content:
            application/json: {}
      security:
      - bearerAuth: []
  /absence-requests/my:
    get:
      tags:
      - "Absence requests"
      summary: "List the caller's own absence requests"
      description: "Returns every absence and vacation request belonging to the caller,\
        \ in any state (pending, approved, rejected, done). Does not include requests\
        \ the caller manages for their direct reports — for those see `/team`."
      operationId: "getMyRequests"
      responses:
        "200":
          description: "List of the caller's requests. Empty array if there are none."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/AbsenceRequestDto"
        "401":
          description: "Missing or invalid bearer token."
          content:
            application/json: {}
        "403":
          description: "Caller's account is inactive."
          content:
            application/json: {}
        "500":
          description: "Unexpected server error."
          content:
            application/json: {}
      security:
      - bearerAuth: []
  /absence-requests/pending-count:
    get:
      tags:
      - "Absence requests"
      summary: "Pending request counts for the caller's team"
      description: "Returns two counts for the caller's direct reports: open absence\
        \ requests awaiting the caller's approval, and open cancellation requests.\
        \ Used to show notification badges on the manager dashboard. Returns zeros\
        \ when the caller has no direct reports."
      operationId: "getPendingCount"
      responses:
        "200":
          description: "Counts returned."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AbsencePendingCountResponse"
        "401":
          description: "Missing or invalid bearer token."
          content:
            application/json: {}
        "403":
          description: "Caller is not a manager or admin."
          content:
            application/json: {}
        "500":
          description: "Unexpected server error."
          content:
            application/json: {}
      security:
      - bearerAuth: []
  /absence-requests/team:
    get:
      tags:
      - "Absence requests"
      summary: "List absence requests of the caller's direct reports"
      description: "Returns every absence request belonging to employees the caller\
        \ manages (admins see all company users), across all states. Each entry includes\
        \ the employee's display name and ID so the result can be rendered as a team\
        \ calendar or review list. Empty array if the caller has no direct reports."
      operationId: "getTeamRequests"
      responses:
        "200":
          description: "List of team requests. Empty array if none."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/AbsenceRequestDto"
        "401":
          description: "Missing or invalid bearer token."
          content:
            application/json: {}
        "403":
          description: "Caller is not a manager or admin."
          content:
            application/json: {}
        "500":
          description: "Unexpected server error."
          content:
            application/json: {}
      security:
      - bearerAuth: []
  /absence-requests/{id}/reject:
    post:
      tags:
      - "Absence requests"
      summary: "Reject a pending absence request"
      description: "Marks a pending absence request as rejected. An optional rejection\
        \ reason may be passed in the body — it is stored and shown to the requesting\
        \ employee. Callers must be an admin or the direct manager of the request's\
        \ owner."
      operationId: "reject"
      parameters:
      - name: "id"
        in: "path"
        description: "Internal ID of the absence request to reject."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 12345
      requestBody:
        description: "Optional body containing a `reason` string shown to the employee."
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RejectRequest"
      responses:
        "200":
          description: "Request after the rejection has been recorded."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AbsenceRequestDto"
        "401":
          description: "Missing or invalid bearer token."
          content:
            application/json: {}
        "403":
          description: "Caller is not a manager or admin, or is not permitted to act\
            \ on this particular request."
          content:
            application/json: {}
        "404":
          description: "No request with the given `id` exists for the caller's company."
          content:
            application/json: {}
        "409":
          description: "Request is not in `pending` state."
          content:
            application/json: {}
        "500":
          description: "Unexpected server error while updating the request."
          content:
            application/json: {}
      security:
      - bearerAuth: []
  /absence-types:
    get:
      tags:
      - "Absence types"
      summary: "List absence types"
      description: "Returns all active absence types the authenticated user is allowed\
        \ to submit."
      operationId: "getAbsenceTypes"
      responses:
        "200":
          description: "List of absence types."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/AbsenceTypeDto"
        "401":
          description: "Missing or invalid bearer token."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /cancellation-requests/{id}/approve:
    post:
      tags:
      - "Cancellation requests"
      summary: "Approve a cancellation request"
      description: "Approves the pending cancellation request for the given absence.\
        \ The absence is deleted and notification emails are sent. Requires `MANAGER`\
        \ or `ADMIN` role."
      operationId: "approve_1"
      parameters:
      - name: "id"
        in: "path"
        description: "Absence ID."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 42
      responses:
        "200":
          description: "Cancellation approved; returns the deleted absence snapshot."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CancellationRequestDto"
        "401":
          description: "Missing or invalid bearer token."
        "403":
          description: "Caller is not a manager or admin, or does not manage the absence\
            \ owner."
        "404":
          description: "Absence not found."
        "409":
          description: "No pending cancellation request for this absence."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /cancellation-requests:
    get:
      tags:
      - "Cancellation requests"
      summary: "List pending cancellation requests"
      description: "Returns all pending cancellation requests visible to the authenticated\
        \ manager or admin. Requires `MANAGER` or `ADMIN` role."
      operationId: "getList"
      responses:
        "200":
          description: "List of pending cancellation requests."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/CancellationRequestDto"
        "401":
          description: "Missing or invalid bearer token."
        "403":
          description: "Caller is not a manager or admin."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /cancellation-requests/pending-count:
    get:
      tags:
      - "Cancellation requests"
      summary: "Get pending cancellation count"
      description: "Returns the number of cancellation requests waiting for a manager\
        \ or admin decision. Requires `MANAGER` or `ADMIN` role."
      operationId: "getPendingCount_1"
      responses:
        "200":
          description: "Pending count."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CancellationPendingCountResponse"
        "401":
          description: "Missing or invalid bearer token."
        "403":
          description: "Caller is not a manager or admin."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /cancellation-requests/{id}/reject:
    post:
      tags:
      - "Cancellation requests"
      summary: "Reject a cancellation request"
      description: "Rejects the pending cancellation request. The absence is kept;\
        \ the employee is notified by email. Requires `MANAGER` or `ADMIN` role."
      operationId: "reject_1"
      parameters:
      - name: "id"
        in: "path"
        description: "Absence ID."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 42
      responses:
        "200":
          description: "Cancellation rejected; returns the updated absence."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CancellationRequestDto"
        "401":
          description: "Missing or invalid bearer token."
        "403":
          description: "Caller is not a manager or admin, or does not manage the absence\
            \ owner."
        "404":
          description: "Absence not found."
        "409":
          description: "No pending cancellation request for this absence."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /categories:
    get:
      tags:
      - "Categories"
      summary: "List categories"
      description: "Returns all active categories along with the user's default category\
        \ and whether a category is required when saving a time entry."
      operationId: "getCategories"
      responses:
        "200":
          description: "Category list with default and obligation flag."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CategoriesResponse"
        "401":
          description: "Missing or invalid bearer token."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /oauth2/token:
    post:
      tags:
      - "Authentication"
      summary: "Issue or refresh a token"
      description: "Supports five grant types:\n\n**`grant_type=password`** — authenticates\
        \ with `username` and `password`. Returns an access token and refresh token\
        \ on success. If the account has 2FA enabled, returns an MFA challenge (`mfa_required=true`,\
        \ `mfa_token`, `mfa_expires_in`) instead — complete the flow with `grant_type=mfa_totp`.\n\
        \n**`grant_type=refresh_token`** — exchanges a valid `refresh_token` for a\
        \ new access token and rotates the refresh token (each refresh token can be\
        \ used exactly once).\n\n**`grant_type=mfa_totp`** — second step of the 2FA\
        \ flow. Submit the `mfa_token` from the password-grant challenge together\
        \ with the current `totp_code` from the user's authenticator app. Returns\
        \ an access token and refresh token on success.\n\n**`grant_type=sso_id_token`**\
        \ — single sign-on via an ID token the mobile app already obtained from the\
        \ SSO provider. Submit `provider=google` and the provider's `id_token` (a\
        \ JWT). Timebutler verifies the JWT's signature, issuer and audience and returns\
        \ an access token and refresh token for the linked Timebutler account.\n\n\
        **`grant_type=sso_code`** — single sign-on via an authorization code the mobile\
        \ app obtained from the SSO provider's consent screen. Submit `provider=microsoft`\
        \ or `provider=slack`, the `code` from the provider, and the exact `redirect_uri`\
        \ used in the authorize request. For Microsoft public-client app registrations,\
        \ also send the PKCE `code_verifier` whose SHA-256 hash was used as `code_challenge`\
        \ in the authorize step. Timebutler exchanges the code with the provider,\
        \ identifies the user and returns the token pair.\n\nAll grant types also\
        \ require `client_id`. Use `client_id=public` to authenticate without a secret,\
        \ or supply both `client_id` and `client_secret` for a registered confidential\
        \ client."
      operationId: "token"
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: "object"
              properties:
                grant_type:
                  type: "string"
                  description: "OAuth 2.0 grant type. Supported values: `password`,\
                    \ `refresh_token`, `mfa_totp`, `sso_id_token`, `sso_code`."
                  example: "password"
                username:
                  type: "string"
                  description: "User's email address. Required for `grant_type=password`."
                  example: "user@example.com"
                password:
                  type: "string"
                  description: "User's password. Required for `grant_type=password`."
                  example: "s3cr3t"
                refresh_token:
                  type: "string"
                  description: "Refresh token obtained from a previous token response.\
                    \ Required for `grant_type=refresh_token`."
                  example: "rt-a1b2c3d4e5f6..."
                client_id:
                  type: "string"
                  description: "Client identifier. Use `public` for unauthenticated\
                    \ public clients."
                  example: "public"
                client_secret:
                  type: "string"
                  description: "Client secret. Required for confidential clients;\
                    \ omit when using `client_id=public`."
                  example: "my-secret"
                mfa_token:
                  type: "string"
                  description: "MFA challenge token received from a `password` grant\
                    \ when 2FA is required. Required for `grant_type=mfa_totp`."
                totp_code:
                  type: "string"
                  description: "Current TOTP code from the user's authenticator app.\
                    \ Required for `grant_type=mfa_totp`."
                provider:
                  type: "string"
                  description: "SSO provider identifier. Required for `grant_type=sso_id_token`\
                    \ (`google`) and `grant_type=sso_code` (`microsoft`, `slack`)."
                  example: "google"
                id_token:
                  type: "string"
                  description: "SSO ID token issued by the provider (e.g. a Google\
                    \ ID token JWT). Required for `grant_type=sso_id_token`."
                code:
                  type: "string"
                  description: "Authorization code returned by the SSO provider's\
                    \ authorize endpoint. Required for `grant_type=sso_code`."
                redirect_uri:
                  type: "string"
                  description: "Redirect URI that was used when obtaining the authorization\
                    \ code. Must match the value registered with the SSO provider.\
                    \ Required for `grant_type=sso_code`."
                  example: "https://app.example.com/sso/callback"
                code_verifier:
                  type: "string"
                  description: "PKCE code verifier — the plaintext value whose SHA-256\
                    \ hash was sent as `code_challenge` in the authorization request.\
                    \ Required for `grant_type=sso_code` when the SSO provider's app\
                    \ registration is a public client (e.g. Microsoft Azure AD mobile\
                    \ app)."
            examples:
              password-grant:
                summary: "Password grant"
                value: "grant_type=password&username=user%40example.com&password=s3cr3t&client_id=public"
              refresh-token-grant:
                summary: "Refresh token grant"
                value: "grant_type=refresh_token&refresh_token=550e8400-e29b-41d4-a716-446655440000&client_id=public"
              mfa-totp-grant:
                summary: "MFA TOTP grant (2FA second step)"
                value: "grant_type=mfa_totp&mfa_token=mfa-a1b2c3d4e5f6&totp_code=123456&client_id=public"
      responses:
        "200":
          description: "Token issued successfully, or MFA challenge returned (password\
            \ grant with 2FA enabled). The response is an `OauthTokenResponse` (access\
            \ token + refresh token) for `refresh_token` and `mfa_totp` grants, or\
            \ an `MfaChallengeResponse` (MFA challenge) for a `password` grant when\
            \ the account has 2FA enabled."
          content:
            application/json:
              schema:
                oneOf:
                - $ref: "#/components/schemas/OauthTokenResponse"
                - $ref: "#/components/schemas/MfaChallengeResponse"
        "400":
          description: "Invalid request, unsupported grant type, or invalid credentials."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OauthErrorResponse"
        "403":
          description: "Access denied due to IP address restriction."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OauthErrorResponse"
        "429":
          description: "Too many requests from this IP address. Try again in 1 minute."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OauthErrorResponse"
        "500":
          description: "Unexpected server error."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OauthErrorResponse"
  /projects:
    get:
      tags:
      - "Projects"
      summary: "List projects"
      description: "Returns all active projects. The user's favourites appear first,\
        \ followed by all other active projects. Also returns the user's default project\
        \ and whether a project is required."
      operationId: "getProjects"
      responses:
        "200":
          description: "Project list with default and obligation flag."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectsResponse"
        "401":
          description: "Missing or invalid bearer token."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /substitute-employees:
    get:
      tags:
      - "Substitute employees"
      summary: "List substitute employees"
      description: "Returns all active employees in the company that may serve as\
        \ a substitute for the authenticated user. The authenticated user is excluded\
        \ from the list."
      operationId: "getSubstituteEmployees"
      responses:
        "200":
          description: "List of substitute employees."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/SubstituteEmployeeDto"
        "401":
          description: "Missing or invalid bearer token."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /substitute-requests/{id}/accept:
    post:
      tags:
      - "Substitute requests"
      summary: "Accept a substitute request"
      description: "Accepts the substitute request for the given absence. Only the\
        \ designated substitute may call this endpoint."
      operationId: "accept"
      parameters:
      - name: "id"
        in: "path"
        description: "Absence ID."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 42
      responses:
        "200":
          description: "Request accepted; returns the updated absence."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubstituteRequestDto"
        "401":
          description: "Missing or invalid bearer token."
        "403":
          description: "Caller is not the designated substitute."
        "404":
          description: "Absence not found."
        "409":
          description: "Request cannot be accepted in its current state (already accepted,\
            \ rejected, or no acceptance required)."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /substitute-requests/pending-count:
    get:
      tags:
      - "Substitute requests"
      summary: "Get pending substitute request count"
      description: "Returns the number of substitute requests that are still waiting\
        \ for the authenticated user's response."
      operationId: "getPendingCount_2"
      responses:
        "200":
          description: "Pending count."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubstitutePendingCountResponse"
        "401":
          description: "Missing or invalid bearer token."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /substitute-requests:
    get:
      tags:
      - "Substitute requests"
      summary: "List substitute requests"
      description: "Returns all substitute requests directed at the authenticated\
        \ user, regardless of their status (pending, accepted, rejected)."
      operationId: "getSubstituteRequests"
      responses:
        "200":
          description: "List of substitute requests."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/SubstituteRequestDto"
        "401":
          description: "Missing or invalid bearer token."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /substitute-requests/{id}/reject:
    post:
      tags:
      - "Substitute requests"
      summary: "Reject a substitute request"
      description: "Rejects the substitute request for the given absence. Only the\
        \ designated substitute may call this endpoint."
      operationId: "reject_2"
      parameters:
      - name: "id"
        in: "path"
        description: "Absence ID."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 42
      responses:
        "200":
          description: "Request rejected; returns the updated absence."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubstituteRequestDto"
        "401":
          description: "Missing or invalid bearer token."
        "403":
          description: "Caller is not the designated substitute."
        "404":
          description: "Absence not found."
        "409":
          description: "Request cannot be rejected in its current state."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /time-clock/business-trip-end:
    post:
      tags:
      - "Time clock"
      summary: "End a business trip"
      description: "Marks the end of an active business trip on the current clock.\
        \ Returns 409 if no business trip is currently active."
      operationId: "businessTripEnd"
      responses:
        "200":
          description: "Business trip ended; returns current status."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeClockStatusDto"
        "400":
          description: "Business trips not enabled, or time tracking not active."
        "401":
          description: "Missing or invalid bearer token."
        "409":
          description: "No business trip is currently active."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /time-clock/business-trip-start:
    post:
      tags:
      - "Time clock"
      summary: "Start a business trip"
      description: "Marks the start of a business trip on the currently running or\
        \ paused clock. Returns 400 if business trips are not enabled for this company;\
        \ returns 409 if the clock is not active or a business trip is already in\
        \ progress."
      operationId: "businessTripStart"
      responses:
        "200":
          description: "Business trip started; returns current status."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeClockStatusDto"
        "400":
          description: "Business trips not enabled, or time tracking not active."
        "401":
          description: "Missing or invalid bearer token."
        "409":
          description: "Clock is not running/paused, or a business trip is already\
            \ active."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /time-clock/cancel:
    post:
      tags:
      - "Time clock"
      summary: "Cancel the time clock"
      description: "Discards the current clock session without saving a time entry.\
        \ Returns 409 if the clock is already `idle`."
      operationId: "cancel"
      responses:
        "200":
          description: "Clock cancelled; returns `idle` status."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeClockStatusDto"
        "400":
          description: "Time tracking not active for this company."
        "401":
          description: "Missing or invalid bearer token."
        "409":
          description: "Clock is already `idle`."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /time-clock/status:
    get:
      tags:
      - "Time clock"
      summary: "Get time clock status"
      description: "Returns the current state of the time clock: `idle`, `running`,\
        \ `paused`, or `waiting`. Also returns elapsed time, pause time, and business-trip\
        \ flag."
      operationId: "getStatus"
      responses:
        "200":
          description: "Current clock status."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeClockStatusDto"
        "400":
          description: "Time tracking not active for this company."
        "401":
          description: "Missing or invalid bearer token."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /time-clock/pause:
    post:
      tags:
      - "Time clock"
      summary: "Pause the time clock"
      description: "Pauses a running time clock. Returns 409 if the clock is not in\
        \ `running` state."
      operationId: "pause"
      responses:
        "200":
          description: "Clock paused; returns current status."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeClockStatusDto"
        "400":
          description: "Time tracking not active for this company."
        "401":
          description: "Missing or invalid bearer token."
        "409":
          description: "Clock is not running."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /time-clock/resume:
    post:
      tags:
      - "Time clock"
      summary: "Resume the time clock"
      description: "Resumes a paused time clock. Returns 409 if the clock is not in\
        \ `paused` state."
      operationId: "resume"
      responses:
        "200":
          description: "Clock resumed; returns current status."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeClockStatusDto"
        "400":
          description: "Time tracking not active for this company."
        "401":
          description: "Missing or invalid bearer token."
        "409":
          description: "Clock is not paused."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /time-clock/start:
    post:
      tags:
      - "Time clock"
      summary: "Start the time clock"
      description: "Starts the time clock for the authenticated user. Returns 409\
        \ if the clock is already running. The clock may enter `waiting` state if\
        \ a minimum start interval is configured."
      operationId: "start"
      responses:
        "200":
          description: "Clock started; returns current status."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeClockStatusDto"
        "400":
          description: "Time tracking not active for this company."
        "401":
          description: "Missing or invalid bearer token."
        "403":
          description: "User is not permitted to create time entries."
        "409":
          description: "Clock is already running, or start conditions are not met."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /time-clock/stop:
    post:
      tags:
      - "Time clock"
      summary: "Stop and save the time clock"
      description: "Stops the running or paused clock and saves the resulting time\
        \ entry. An optional project ID and remarks can be provided. If the clock\
        \ was in `waiting` state it is discarded without saving and 204 is returned.\
        \ Returns 409 if the total duration is too short (< 1 minute) or if a duplicate\
        \ entry is detected.\n\nIf the session violates the company's break-time regulation\
        \ and no `breakRegulation` decision was sent, the entry is **not** saved:\
        \ the response has `outcome: confirmationRequired` and a `breakRegulation`\
        \ object describing the required change. Re-send the stop with `breakRegulation:\
        \ apply` (accept the correction) or `breakRegulation: ignore` (save unchanged).\
        \ Clients should only offer `ignore` when the regulation is not obligatory\
        \ (`breakRegulation.obligatory` in the response); the server does not currently\
        \ enforce this. When the company has auto-apply configured, the correction\
        \ is applied on the first stop without a decision."
      operationId: "stop"
      requestBody:
        description: "Optional project ID, category ID, and remarks for the saved\
          \ entry."
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TimeClockStopRequest"
      responses:
        "200":
          description: "Either the saved entry details (`outcome: saved`) or a break-regulation\
            \ confirmation request (`outcome: confirmationRequired`)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeClockStopResponse"
        "204":
          description: "Clock was in `waiting` state and was discarded without saving."
        "400":
          description: "Time tracking not active for this company."
        "401":
          description: "Missing or invalid bearer token."
        "409":
          description: "Clock is not active, duration is too short, or duplicate entry\
            \ detected."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /time-entry-requests/{id}/approve:
    post:
      tags:
      - "Time entry requests"
      summary: "Approve a time-entry request"
      description: "Approves the pending change or deletion request for the given\
        \ time entry. For deletion requests the entry is removed; for change requests\
        \ the entry is updated to the proposed values. Requires `MANAGER` or `ADMIN`\
        \ role."
      operationId: "approve_2"
      parameters:
      - name: "id"
        in: "path"
        description: "Time entry ID."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 123
      responses:
        "200":
          description: "Request approved; returns the updated time entry."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeEntryRequestDto"
        "400":
          description: "Time tracking not active for this company."
        "401":
          description: "Missing or invalid bearer token."
        "403":
          description: "Caller is not a manager or admin, or only admins may approve\
            \ in this company."
        "404":
          description: "Time entry not found."
        "409":
          description: "No pending request, or the proposed change has a scheduling\
            \ conflict."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /time-entry-requests:
    get:
      tags:
      - "Time entry requests"
      summary: "List pending time-entry requests"
      description: "Returns all time-entry change/deletion requests in the manager's\
        \ team that are waiting for approval. Requires `MANAGER` or `ADMIN` role.\
        \ Returns an empty list if time tracking is not active or the caller is a\
        \ manager in an admin-only-approval company."
      operationId: "getList_1"
      responses:
        "200":
          description: "List of pending time-entry requests."
          content:
            application/json:
              schema:
                type: "array"
                items:
                  $ref: "#/components/schemas/TimeEntryRequestDto"
        "401":
          description: "Missing or invalid bearer token."
        "403":
          description: "Caller is not a manager or admin."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /time-entry-requests/{id}/reject:
    post:
      tags:
      - "Time entry requests"
      summary: "Reject a time-entry request"
      description: "Rejects the pending change or deletion request for the given time\
        \ entry. An optional rejection reason can be included in the request body.\
        \ The employee is notified by email. Requires `MANAGER` or `ADMIN` role."
      operationId: "reject_3"
      parameters:
      - name: "id"
        in: "path"
        description: "Time entry ID."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 123
      requestBody:
        description: "Optional rejection reason."
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RejectRequest"
      responses:
        "200":
          description: "Request rejected; returns the time entry in its current state."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeEntryRequestDto"
        "400":
          description: "Time tracking not active for this company."
        "401":
          description: "Missing or invalid bearer token."
        "403":
          description: "Caller is not a manager or admin, or only admins may reject\
            \ in this company."
        "404":
          description: "Time entry not found."
        "409":
          description: "No pending request for this entry."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /time-entries/{id}:
    delete:
      tags:
      - "Time entries"
      summary: "Delete a time entry"
      description: "Deletes the specified time entry. If the company's time-tracking\
        \ settings require manager approval for deletions, a deletion request is submitted\
        \ instead of an immediate delete. The response field `result` is either `deleted`\
        \ or `deletion_requested`."
      operationId: "delete_1"
      parameters:
      - name: "id"
        in: "path"
        description: "Time entry ID."
        required: true
        schema:
          type: "integer"
          format: "int32"
        example: 123
      responses:
        "200":
          description: "Entry deleted or deletion request submitted."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteResultResponse"
        "400":
          description: "Invalid ID or time tracking is not active for this company."
        "401":
          description: "Missing or invalid bearer token."
        "403":
          description: "Caller is not allowed to delete this entry."
        "404":
          description: "Time entry not found."
        "409":
          description: "Entry is locked by the month-close restriction."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /time-entries/pending-count:
    get:
      tags:
      - "Time entries"
      summary: "Get pending time-entry count"
      description: "Returns the number of time entries in the manager's team that\
        \ are awaiting approval. Requires `MANAGER` or `ADMIN` role."
      operationId: "getPendingCount_3"
      responses:
        "200":
          description: "Pending count."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeEntryPendingCountResponse"
        "401":
          description: "Missing or invalid bearer token."
        "403":
          description: "Caller is not a manager or admin."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /time-tracking/clock-entry:
    post:
      tags:
      - "Time tracking"
      summary: "Submit a completed clock entry"
      description: "Creates a time entry from explicit start and end Unix timestamps\
        \ (milliseconds). Start and end must be on the same calendar day. If the company\
        \ requires manager approval for new entries, the entry is created with status\
        \ `pending`; otherwise it is immediately `done`."
      operationId: "createClockEntry"
      requestBody:
        description: "Clock entry with start/end timestamps and optional project and\
          \ remarks."
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ClockEntryRequest"
        required: true
      responses:
        "200":
          description: "Entry created; returns the entry ID and worked/pause minutes."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClockEntryResponse"
        "400":
          description: "Missing or invalid request body, invalid timestamps, or time\
            \ tracking not active."
        "401":
          description: "Missing or invalid bearer token."
        "403":
          description: "User is not allowed to create time entries."
        "422":
          description: "Remarks exceed maximum length."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /time-tracking/time-off-summary:
    get:
      tags:
      - "Time tracking"
      summary: "Get time-off summary"
      description: "Returns the authenticated user's remaining and total vacation\
        \ days for the current year, plus the number of illness days taken."
      operationId: "getTimeOffSummary"
      responses:
        "200":
          description: "Time-off summary."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeOffSummaryResponse"
        "401":
          description: "Missing or invalid bearer token."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /time-tracking/work-summary:
    get:
      tags:
      - "Time tracking"
      summary: "Get today's work summary"
      description: "Returns the authenticated user's work commitment, worked minutes,\
        \ remaining minutes, completion percentage, and break minutes for today."
      operationId: "getWorkSummary"
      responses:
        "200":
          description: "Today's work summary."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkSummaryResponse"
        "401":
          description: "Missing or invalid bearer token."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
  /user/profile:
    get:
      tags:
      - "User profile"
      summary: "Get profile"
      description: "Returns the authenticated user's display name, email address,\
        \ role (`USER`, `MANAGER`, `ADMIN`), department, branch office, and avatar\
        \ URL."
      operationId: "getProfile"
      responses:
        "200":
          description: "Profile data."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserProfileResponse"
        "401":
          description: "Missing or invalid bearer token."
        "500":
          description: "Unexpected server error."
      security:
      - bearerAuth: []
components:
  schemas:
    AbsenceRequestDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Internal ID of the absence request."
          example: "12345"
        type:
          type: "string"
          description: "Absence type code (3 characters), e.g. `\"URL\"` for vacation."
          example: "URL"
        startDate:
          type: "string"
          description: "First day of the absence in ISO-8601 format (`YYYY-MM-DD`)."
          example: "2025-08-01"
        endDate:
          type: "string"
          description: "Last day of the absence in ISO-8601 format (`YYYY-MM-DD`)."
          example: "2025-08-05"
        days:
          type: "number"
          description: "Number of working days the absence spans, calculated according\
            \ to the employee's working-days schedule."
          format: "double"
          example: 4.0
        status:
          type: "string"
          description: "Current status of the request. One of `pending` (awaiting\
            \ manager approval), `approved`, or `rejected`."
          example: "pending"
          enum:
          - "pending"
          - "approved"
          - "rejected"
        halfDay:
          type: "string"
          description: "Half-day indicator. `\"morning\"` or `\"afternoon\"` for half-day\
            \ requests; `null` for full-day absences."
          nullable: true
          example: "morning"
          enum:
          - "morning"
          - "afternoon"
        substitutePending:
          type: "boolean"
          description: "Whether the substitute employee's confirmation is still pending.\
            \ `null` if no substitute was set."
          nullable: true
          example: false
        medicalCertificateProvided:
          type: "boolean"
          description: "Whether a medical certificate has been uploaded for this absence.\
            \ Only present for absence types that require a sick note; `null` otherwise."
          nullable: true
          example: false
        employeeName:
          type: "string"
          description: "Display name of the employee who submitted the request. Only\
            \ present in responses from `GET /team`; `null` in `GET /my` responses."
          nullable: true
          example: "Maria Müller"
        employeeId:
          type: "string"
          description: "Internal ID of the employee who submitted the request. Only\
            \ present in responses from `GET /team`; `null` in `GET /my` responses."
          nullable: true
          example: "42"
    CreateAbsenceRequest:
      required:
      - "absenceTypeId"
      - "endDate"
      - "startDate"
      type: "object"
      properties:
        absenceTypeId:
          type: "string"
          description: "Absence type code (3 characters), e.g. `\"URL\"` for vacation.\
            \ Must match an active type from `GET /absence-types`."
          example: "URL"
        startDate:
          type: "string"
          description: "First day of the absence in ISO-8601 format (`YYYY-MM-DD`)."
          example: "2025-08-01"
        endDate:
          type: "string"
          description: "Last day of the absence in ISO-8601 format (`YYYY-MM-DD`).\
            \ Must be on or after `startDate`. For half-day requests must equal `startDate`."
          example: "2025-08-05"
        halfDay:
          type: "string"
          description: "Set to `\"morning\"` or `\"afternoon\"` to request a single\
            \ half-day. When set, `startDate` and `endDate` must be the same day.\
            \ Omit or send `null` for a full-day absence."
          example: "morning"
          enum:
          - "morning"
          - "afternoon"
        comments:
          type: "string"
          description: "Optional free-text note visible to the employee's managers."
          example: "Holiday trip"
        substituteEmployeeId:
          type: "string"
          description: "Employee ID of the designated substitute. Must belong to the\
            \ same company. Omit or send `null` if no substitute is needed."
          example: "42"
    DeleteAbsenceRequest:
      type: "object"
      properties:
        reason:
          type: "string"
          description: "Optional free-text reason for the cancellation. Required when\
            \ the company's configuration mandates a reason for cancellation requests."
          example: "Plans changed"
    DeleteResultResponse:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Internal ID of the affected record."
          example: "12345"
        result:
          type: "string"
          description: "Outcome of the operation. `\"deleted\"` if the record was\
            \ removed outright; `\"cancellation_requested\"` if a cancellation request\
            \ was created instead; `\"deletion_requested\"` if a deletion request\
            \ was created (for time entries that require approval)."
          example: "deleted"
          enum:
          - "deleted"
          - "cancellation_requested"
          - "deletion_requested"
    AbsencePendingCountResponse:
      type: "object"
      properties:
        numberOfAbsenceRequests:
          type: "integer"
          description: "Number of absence requests in the caller's team that are waiting\
            \ for the caller's approval."
          format: "int32"
          example: 3
        numberOfAbsenceRequestCancellations:
          type: "integer"
          description: "Number of cancellation requests in the caller's team that\
            \ are waiting for approval."
          format: "int32"
          example: 1
    RejectRequest:
      type: "object"
      properties:
        reason:
          type: "string"
          description: "Optional free-text reason for the rejection. Stored on the\
            \ request and shown to the employee."
          example: "Insufficient staffing during that period"
    UpdateAbsenceRequest:
      type: "object"
      properties:
        absenceTypeId:
          type: "string"
          description: "New absence type code (3 characters). Send `null` to keep\
            \ the existing type."
          example: "URL"
        startDate:
          type: "string"
          description: "New start date in ISO-8601 format (`YYYY-MM-DD`). Send `null`\
            \ to keep the existing value."
          example: "2025-08-01"
        endDate:
          type: "string"
          description: "New end date in ISO-8601 format (`YYYY-MM-DD`). Must be on\
            \ or after `startDate`. Send `null` to keep the existing value."
          example: "2025-08-05"
        halfDay:
          type: "string"
          description: "New half-day setting. `\"morning\"` or `\"afternoon\"` to\
            \ switch to a half-day; empty string `\"\"` to remove half-day and make\
            \ it a full day; `null` to keep the existing value."
          example: "morning"
          enum:
          - "morning"
          - "afternoon"
          - ""
        comments:
          type: "string"
          description: "New free-text comment. Send `null` to keep the existing comment;\
            \ send an empty string to clear it."
          example: "Rescheduled trip"
        substituteEmployeeId:
          type: "string"
          description: "New substitute employee ID. Send `null` to keep the existing\
            \ substitute; send `\"0\"` or an empty string to remove the substitute."
          example: "42"
    AbsenceTypeDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Absence type code (3 characters) used when creating or updating\
            \ an absence request."
          example: "URL"
        name:
          type: "string"
          description: "Human-readable name of the absence type in the user's language."
          example: "Urlaub"
        needsApproval:
          type: "boolean"
          description: "Whether requests of this type require manager approval before\
            \ they are considered approved. If `false`, the request is approved immediately\
            \ on creation."
          example: true
        requiresMedicalCertificate:
          type: "boolean"
          description: "Whether a medical certificate (sick note) must be uploaded\
            \ for absences of this type."
          example: false
    CancellationRequestDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Internal ID of the absence request being cancelled."
          example: "12345"
        employeeId:
          type: "string"
          description: "Internal ID of the employee who submitted the original absence\
            \ request."
          example: "42"
        employeeName:
          type: "string"
          description: "Display name of the employee who submitted the original absence\
            \ request."
          example: "Maria Müller"
        absenceType:
          type: "string"
          description: "Absence type code (3 characters) of the request being cancelled."
          example: "URL"
        startDate:
          type: "string"
          description: "Start date of the absence being cancelled, in ISO-8601 format\
            \ (`YYYY-MM-DD`)."
          example: "2025-08-01"
        endDate:
          type: "string"
          description: "End date of the absence being cancelled, in ISO-8601 format\
            \ (`YYYY-MM-DD`)."
          example: "2025-08-05"
        days:
          type: "number"
          description: "Number of working days the absence spans."
          format: "double"
          example: 4.0
        status:
          type: "string"
          description: "Current status of the cancellation request. One of `pending`\
            \ (awaiting approval) or `approved`."
          example: "pending"
          enum:
          - "pending"
          - "approved"
        cancelComments:
          type: "string"
          description: "Optional free-text reason provided by the employee when requesting\
            \ the cancellation."
          nullable: true
          example: "Plans changed"
    CancellationPendingCountResponse:
      type: "object"
      properties:
        numberOfCancellationRequests:
          type: "integer"
          description: "Number of cancellation requests in the caller's team that\
            \ are waiting for approval."
          format: "int32"
          example: 2
    CategoriesResponse:
      type: "object"
      properties:
        categories:
          type: "array"
          description: "List of categories available to the authenticated user's company."
          items:
            $ref: "#/components/schemas/CategoryDto"
        defaultCategoryId:
          type: "string"
          description: "ID of the category pre-selected by default when creating a\
            \ time entry. `null` if no default is configured."
          nullable: true
          example: "7"
        isCategoryMandatory:
          type: "boolean"
          description: "Whether the employee must select a category when creating\
            \ a time entry."
          example: false
    CategoryDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Internal ID of the category, used when submitting a time entry."
          example: "7"
        name:
          type: "string"
          description: "Human-readable name of the category."
          example: "Internal project"
      description: "List of categories available to the authenticated user's company."
    OauthTokenResponse:
      type: "object"
      properties:
        access_token:
          type: "string"
          description: "JWT access token. Pass as `Authorization: Bearer <token>`\
            \ on subsequent requests."
          example: "eyJhbGciOiJSUzI1NiJ9..."
        refresh_token:
          type: "string"
          description: "Opaque refresh token. Exchange it for a new access token via\
            \ `grant_type=refresh_token`. Each refresh token can be used exactly once."
          example: "rt-a1b2c3d4e5f6..."
        token_type:
          type: "string"
          description: "Token type. Always `Bearer`."
          example: "Bearer"
        expires_in:
          type: "integer"
          description: "Access token lifetime in seconds."
          format: "int32"
          example: 3600
        scope:
          type: "string"
          description: "Granted scope."
          example: "read"
    MfaChallengeResponse:
      type: "object"
      properties:
        mfa_required:
          type: "boolean"
          description: "Always `true` — indicates a 2FA step is required."
          example: true
        mfa_token:
          type: "string"
          description: "Short-lived token identifying this MFA challenge. Pass it\
            \ as `mfa_token` with `grant_type=mfa_totp`."
          example: "mfa-a1b2c3d4e5f6..."
        mfa_expires_in:
          type: "integer"
          description: "Lifetime of the MFA token in seconds."
          format: "int32"
          example: 300
      description: "Returned by `grant_type=password` when the account has 2FA enabled.\
        \ Exchange the `mfa_token` for a full token pair by calling this endpoint\
        \ again with `grant_type=mfa_totp`."
    OauthErrorResponse:
      type: "object"
      properties:
        error:
          type: "string"
          description: "OAuth 2.0 error code (e.g. `invalid_grant`, `invalid_client`)."
          example: "invalid_grant"
        error_description:
          type: "string"
          description: "Human-readable description of the error."
          example: "Invalid username or password"
    ProjectDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Internal ID of the project, used when submitting a time entry."
          example: "5"
        name:
          type: "string"
          description: "Human-readable name of the project."
          example: "Website Relaunch"
        isFavorite:
          type: "boolean"
          description: "Whether this project is marked as a favourite by the authenticated\
            \ user."
          example: true
      description: "List of projects available to the authenticated user's company."
    ProjectsResponse:
      type: "object"
      properties:
        projects:
          type: "array"
          description: "List of projects available to the authenticated user's company."
          items:
            $ref: "#/components/schemas/ProjectDto"
        defaultProjectId:
          type: "string"
          description: "ID of the project pre-selected by default when creating a\
            \ time entry. `null` if no default is configured."
          nullable: true
          example: "5"
        isProjectMandatory:
          type: "boolean"
          description: "Whether the employee must select a project when creating a\
            \ time entry."
          example: true
    SubstituteEmployeeDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Internal employee ID, used as `substituteEmployeeId` when\
            \ creating an absence request."
          example: "42"
        name:
          type: "string"
          description: "Display name of the employee."
          example: "Thomas Schmidt"
        department:
          type: "string"
          description: "Department the employee belongs to. `null` if the company\
            \ does not use departments."
          nullable: true
          example: "Engineering"
    SubstituteRequestDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Internal ID of the absence request for which the substitute\
            \ was requested."
          example: "12345"
        employeeId:
          type: "string"
          description: "Internal ID of the employee who is absent and needs a substitute."
          example: "7"
        employeeName:
          type: "string"
          description: "Display name of the absent employee."
          example: "Anna Bauer"
        absenceType:
          type: "string"
          description: "Absence type code (3 characters) of the absence for which\
            \ this substitute was requested."
          example: "URL"
        startDate:
          type: "string"
          description: "Start date of the absence, in ISO-8601 format (`YYYY-MM-DD`)."
          example: "2025-08-01"
        endDate:
          type: "string"
          description: "End date of the absence, in ISO-8601 format (`YYYY-MM-DD`)."
          example: "2025-08-05"
        days:
          type: "number"
          description: "Number of working days the absence spans."
          format: "double"
          example: 4.0
        status:
          type: "string"
          description: "Current status of the substitute confirmation. One of `pending`\
            \ (awaiting the substitute's confirmation) or `confirmed`."
          example: "pending"
          enum:
          - "pending"
          - "confirmed"
    SubstitutePendingCountResponse:
      type: "object"
      properties:
        numberOfSubstituteRequests:
          type: "integer"
          description: "Number of substitute requests awaiting the caller's confirmation."
          format: "int32"
          example: 1
    TimeClockStatusDto:
      type: "object"
      properties:
        status:
          type: "string"
          description: "Current state of the time clock. One of `idle` (not running),\
            \ `running` (clocked in), `paused`, or `waiting` (started but held until\
            \ a minimum interval passes)."
          example: "running"
          enum:
          - "idle"
          - "running"
          - "paused"
          - "waiting"
        startTimestamp:
          type: "integer"
          description: "Unix timestamp in milliseconds when the current clock-in session\
            \ started. `null` when `status` is `idle`."
          format: "int64"
          nullable: true
          example: 1753700400000
        pauseTimestamp:
          type: "integer"
          description: "Unix timestamp in milliseconds when the current pause started.\
            \ `null` if the clock is not paused."
          format: "int64"
          nullable: true
          example: 1753714800000
        workTimeElapsedSeconds:
          type: "integer"
          description: "Total working time elapsed in the current session, in seconds\
            \ (excludes breaks)."
          format: "int32"
          example: 14400
        breakElapsedSeconds:
          type: "integer"
          description: "Duration of the current ongoing break, in seconds. `0` if\
            \ the clock is not paused."
          format: "int32"
          example: 0
        accumulatedBreakSeconds:
          type: "integer"
          description: "Total accumulated break time in the current session, in seconds\
            \ (including any completed breaks)."
          format: "int32"
          example: 1800
        waitSeconds:
          type: "integer"
          description: "Minimum number of seconds remaining before the employee is\
            \ allowed to clock out, based on company policy. `null` if there is no\
            \ minimum."
          format: "int32"
          nullable: true
          example: 0
        isBusinessTripActive:
          type: "boolean"
          description: "Whether a business trip is currently active for this session.\
            \ `false` when no business trip is active or the feature is not enabled."
          example: false
    TimeClockBreakRegulationInfo:
      type: "object"
      properties:
        hasNoRemedy:
          type: "boolean"
          description: "`true` when the violation cannot be resolved automatically\
            \ (e.g. the daily maximum working time is already exceeded). The session\
            \ can then only be discarded or, if the regulation is not obligatory,\
            \ saved unchanged with `ignore`."
          example: false
        obligatory:
          type: "boolean"
          description: "`true` when the break regulation is obligatory for this company.\
            \ When obligatory the session must not be saved unchanged; the client\
            \ hides the `ignore` option."
          example: false
        correctionWorkMinutes:
          type: "integer"
          description: "Minutes that would be deducted from working time if the correction\
            \ is applied."
          format: "int32"
          example: 30
        correctionBreakMinutes:
          type: "integer"
          description: "Minutes that would be added to break time if the correction\
            \ is applied."
          format: "int32"
          example: 30
        resultingWorkMinutes:
          type: "integer"
          description: "Resulting net working time in minutes after the correction\
            \ is applied."
          format: "int32"
          example: 450
        resultingBreakMinutes:
          type: "integer"
          description: "Resulting total break time in minutes after the correction\
            \ is applied."
          format: "int32"
          example: 30
        detailText:
          type: "string"
          description: "Human-readable, localized summary of the required change,\
            \ suitable for display."
          example: "Required change: working time -0:30, break +0:30"
      description: "Break-regulation violation details. Present only when `outcome`\
        \ is `confirmationRequired`; null otherwise."
    TimeClockStopResponse:
      type: "object"
      properties:
        outcome:
          type: "string"
          description: "`saved` when the entry was persisted, or `confirmationRequired`\
            \ when a break-regulation decision is needed before saving."
          example: "saved"
          enum:
          - "saved"
          - "confirmationRequired"
        id:
          type: "string"
          description: "Internal ID of the time entry created when the clock was stopped.\
            \ Null when `outcome` is `confirmationRequired`."
          example: "98765"
        workedMinutes:
          type: "integer"
          description: "Net working time recorded for the session, in minutes (total\
            \ duration minus breaks). Null when `outcome` is `confirmationRequired`."
          format: "int32"
          example: 450
        breakMinutes:
          type: "integer"
          description: "Total break time recorded for the session, in minutes. Null\
            \ when `outcome` is `confirmationRequired`."
          format: "int32"
          example: 30
        breakRegulationApplied:
          type: "boolean"
          description: "`true` when a break-regulation correction was applied to the\
            \ saved entry (working time reduced, break increased)."
          example: true
        breakRegulationIgnored:
          type: "boolean"
          description: "`true` when the entry was saved unchanged despite a break-regulation\
            \ violation (the user chose to ignore it)."
          example: false
        breakRegulation:
          $ref: "#/components/schemas/TimeClockBreakRegulationInfo"
    TimeClockStopRequest:
      type: "object"
      properties:
        projectId:
          type: "integer"
          description: "ID of the project to assign the completed session to. Send\
            \ `0` if no project assignment is needed."
          format: "int32"
          example: 5
        categoryId:
          type: "integer"
          description: "ID of the category to assign the completed session to. Send\
            \ `0` if no category assignment is needed."
          format: "int32"
          example: 7
        remarks:
          type: "string"
          description: "Optional free-text note for the completed time entry."
          example: "Code review"
        breakRegulation:
          type: "string"
          description: "Decision on how to handle a break-time-regulation violation.\
            \ Omit on the first stop attempt: if the session violates the break regulation\
            \ the server does not save and instead returns `outcome: confirmationRequired`\
            \ describing the required change. Re-send the stop with `apply` to accept\
            \ the correction (reduce working time and add the missing break) or `ignore`\
            \ to save the session unchanged. Clients should only offer `ignore` when\
            \ the regulation is not obligatory (`obligatory` in the confirmation response);\
            \ the server does not currently enforce this."
          example: "apply"
          enum:
          - "apply"
          - "ignore"
    TimeEntryRequestDto:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Internal ID of the time entry."
          example: "98765"
        employeeId:
          type: "string"
          description: "Internal ID of the employee who submitted the time entry.\
            \ Only present in team/manager views."
          nullable: true
          example: "42"
        employeeName:
          type: "string"
          description: "Display name of the employee who submitted the time entry.\
            \ Only present in team/manager views."
          nullable: true
          example: "Maria Müller"
        date:
          type: "string"
          description: "Date of the time entry in ISO-8601 format (`YYYY-MM-DD`)."
          example: "2025-07-28"
        startTime:
          type: "string"
          description: "Clock-in time in `HH:mm` format."
          example: "08:00"
        endTime:
          type: "string"
          description: "Clock-out time in `HH:mm` format."
          example: "17:00"
        workedMinutes:
          type: "integer"
          description: "Net working time for this entry, in minutes (total duration\
            \ minus breaks)."
          format: "int32"
          example: 480
        breakMinutes:
          type: "integer"
          description: "Total break time for this entry, in minutes."
          format: "int32"
          example: 60
        project:
          type: "string"
          description: "Name of the project assigned to this entry. `null` if no project\
            \ was assigned."
          nullable: true
          example: "Website Relaunch"
        category:
          type: "string"
          description: "Name of the category assigned to this entry. `null` if no\
            \ category was assigned."
          nullable: true
          example: "Internal project"
        remarks:
          type: "string"
          description: "Free-text note attached to this entry. `null` if no remark\
            \ was entered."
          nullable: true
          example: "Sprint planning"
        requestType:
          type: "string"
          description: "Type of request that produced this entry. One of `manual`\
            \ (entered by hand) or `clock` (created by stopping the time clock)."
          example: "manual"
          enum:
          - "manual"
          - "clock"
        status:
          type: "string"
          description: "Approval status of this time entry. One of `pending` (awaiting\
            \ manager approval) or `approved`."
          example: "pending"
          enum:
          - "pending"
          - "approved"
    TimeEntryPendingCountResponse:
      type: "object"
      properties:
        numberOfTimeEntries:
          type: "integer"
          description: "Number of time entries in the caller's team that are waiting\
            \ for approval."
          format: "int32"
          example: 5
    ClockEntryResponse:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Internal ID of the created time entry."
          example: "98765"
        workedMinutes:
          type: "integer"
          description: "Net working time recorded for this entry, in minutes (total\
            \ duration minus breaks)."
          format: "int32"
          example: 480
        breakMinutes:
          type: "integer"
          description: "Total break time recorded for this entry, in minutes."
          format: "int32"
          example: 30
    ClockEntryRequest:
      required:
      - "endTimestamp"
      - "startTimestamp"
      type: "object"
      properties:
        startTimestamp:
          type: "integer"
          description: "Start of the work period as a Unix timestamp in milliseconds."
          format: "int64"
          example: 1753700400000
        endTimestamp:
          type: "integer"
          description: "End of the work period as a Unix timestamp in milliseconds."
          format: "int64"
          example: 1753729200000
        pauseSeconds:
          type: "integer"
          description: "Total break duration within the work period, in seconds."
          format: "int32"
          example: 1800
        projectId:
          type: "integer"
          description: "ID of the project to assign this entry to. Send `0` if no\
            \ project assignment is needed."
          format: "int32"
          example: 5
        remarks:
          type: "string"
          description: "Optional free-text note for this time entry."
          example: "Sprint planning"
    TimeOffSummaryResponse:
      type: "object"
      properties:
        vacationDaysRemaining:
          type: "number"
          description: "Number of vacation days the employee still has available in\
            \ the current year, after accounting for approved and pending requests."
          format: "double"
          example: 12.5
        vacationDaysTotal:
          type: "number"
          description: "Total vacation day entitlement for the current year."
          format: "double"
          example: 30.0
        illnessDaysTaken:
          type: "number"
          description: "Number of sick-leave days taken in the current year."
          format: "double"
          example: 3.0
    WorkSummaryResponse:
      type: "object"
      properties:
        commitmentMinutes:
          type: "integer"
          description: "Total working time the employee is contractually required\
            \ to work in the queried period, in minutes."
          format: "int32"
          example: 2400
        workedMinutes:
          type: "integer"
          description: "Total working time the employee has actually worked in the\
            \ queried period, in minutes."
          format: "int32"
          example: 2280
        remainingMinutes:
          type: "integer"
          description: "Remaining working time still to be worked in the queried period,\
            \ in minutes. Negative values indicate overtime."
          format: "int32"
          example: 120
        percentDone:
          type: "integer"
          description: "Percentage of the contractual working time that has been worked\
            \ so far, rounded to the nearest integer."
          format: "int32"
          example: 95
        breakMinutes:
          type: "integer"
          description: "Total break time recorded in the queried period, in minutes."
          format: "int32"
          example: 300
    UserProfileResponse:
      type: "object"
      properties:
        id:
          type: "string"
          description: "Internal ID of the authenticated user."
          example: "42"
        name:
          type: "string"
          description: "Full display name of the user."
          example: "Maria Müller"
        email:
          type: "string"
          description: "Email address of the user."
          example: "maria.mueller@example.com"
        role:
          type: "string"
          description: "Role of the user within the company. One of `USER`, `MANAGER`,\
            \ or `ADMIN`."
          example: "MANAGER"
          enum:
          - "USER"
          - "MANAGER"
          - "ADMIN"
        department:
          type: "string"
          description: "Name of the department the user belongs to. `null` if the\
            \ company does not use departments."
          nullable: true
          example: "Engineering"
        branchOffice:
          type: "string"
          description: "Name of the branch office the user is assigned to. `null`\
            \ if the company does not use branch offices."
          nullable: true
          example: "Berlin"
        country:
          type: "object"
          description: "Country of the user as configured in their profile. The structure\
            \ depends on the company's configuration."
          nullable: true
        avatar:
          type: "string"
          description: "URL of the user's profile picture. `null` if no avatar has\
            \ been uploaded."
          nullable: true
          example: "/images/avatar/42/abc123.jpg?v=5"
        isBusinessTripEnabled:
          type: "boolean"
          description: "Whether the business trip feature is enabled for this user.\
            \ `null` if the feature is not available in the company's plan."
          nullable: true
          example: true
        isTimeTrackingEnabled:
          type: "boolean"
          description: "Whether time tracking is active for the user's company."
          example: true
  securitySchemes:
    bearerAuth:
      type: "http"
      scheme: "bearer"
      bearerFormat: "JWT"
