openapi: 3.0.3
info:
  title: StopWatch API
  description: |
    An authentication wrapper for controlled access to HubSpot App Object APIs. 
    This API allows programmatic access to StopWatch Time Log records, bypassing 
    standard HubSpot scope restrictions for App Objects.
  version: 1.0.0
servers:
  - url: https://api.threadi.au
    description: Production server

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: |
        Bearer Authorization header of the form Bearer {token}, where {token} is your API key.
        By default, each API key is **rate-limited to 1,000 requests per day** (UTC). If you need a higher daily limit, please submit a [support request](https://www.threadi.au/support?app_name=stopwatch).

  responses:
    UnauthorizedError:
      description: API key is missing or invalid.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error401'
    ForbiddenError:
      description: API key has been deactivated.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error403'
    NotFoundError:
      description: The requested resource was not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error404'
    InternalServerError:
      description: An unexpected error occurred.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error500'
    TooManyRequestsError:
      description: UTC daily API quota exceeded for this API key.
      headers:
        x-stopwatch-ratelimit-reset:
          description: UTC timestamp indicating when the daily quota resets.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error429'
    ServiceUnavailableError:
      description: Service is temporarily busy.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error503'
    MultiStatusResponse:
      description: |
        A Multi-Status response where some requested records were found and others were not. 
        This is returned with a `207` status code.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/BatchReadMultiStatusResponse'
          example:
            completedAt: "2026-03-25T05:55:26.329Z"
            errors:
              - category: "OBJECT_NOT_FOUND"
                context:
                  ids:
                    - "1111"
                message: "Could not get some null objects, they may be deleted or not exist. Check that ids are valid."
                status: "error"
            numErrors: 1
            results:
              - archived: false
                createdAt: "2025-09-03T07:43:50.479Z"
                id: "151993156822"
                properties:
                  a3079614_assoc_record_id: "762476848343"
                  a3079614_billable_hours: "4.00"
                  hs_createdate: "2025-09-03T07:43:50.479Z"
                  hs_lastmodifieddate: "2025-09-03T07:43:51.260Z"
                  hs_object_id: "151993156822"
                updatedAt: "2025-09-03T07:43:51.260Z"
            startedAt: "2026-03-25T05:55:26.300Z"
            status: "COMPLETE"

  schemas:

    Error400:
      type: object
      properties:
        error:
          type: string
          example: HubSpot API Error
        details:
          type: string
          example: <hubspot error as string>

    Error401:
      type: object
      properties:
        error:
          type: string
          example: Invalid API key
    
    Error403:
      type: object
      properties:
        error:
          type: string
          example: API key has been deactivated
    
    Error404:
      type: object
      properties:
        error:
          type: string
          example: HubSpot API Error
        details:
          type: string
          example: |
            <html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\"/>\n<title>Error 404 Not Found</title>\n</head>\n<body><h2>HTTP ERROR 404</h2>\n<p>Resource not found</p>\n</body>\n</html>\n
      
    Error500:
      type: object
      properties:
        error:
          type: string
          example: Internal Server Error
        details:
          type: string
          example: An unexpected error occurred.

    Error429:
      type: object
      properties:
        error:
          type: string
          example: Daily quota exceeded
        details:
          type: string
          example: This API key has reached its UTC daily request limit. Use response header x-stopwatch-ratelimit-reset to know when this quota will reset.

    Error503:
      type: object
      properties:
        error:
          type: string
          example: Service temporarily busy
        details:
          type: string
          example: Unable to process request right now. Please retry shortly.

    BatchReadInput:
      type: object
      required:
        - id
      properties:
        id:
          type: string
          example: "1234567890"
          description: Must align with `idProperty` (if supplied). If `idProperty` is omitted, must reference `hs_object_id`.
    
    FilterGroup:
      type: object
      required:
        - filters
      properties:
        filters:
          type: array
          items:
            $ref: '#/components/schemas/Filter'
    
    Filter:
      type: object
      required:
        - propertyName
        - operator
      properties:
        propertyName:
          type: string
          example: a552213_record_id
          description: The property name to filter on. Must be a valid **StopWatch Time Log** property.
        operator:
          type: string
          example: EQ
          enum: [LT, LTE, GT, GTE, EQ, NEQ, BETWEEN, IN, NOT_IN, HAS_PROPERTY, NOT_HAS_PROPERTY, CONTAINS_TOKEN, NOT_CONTAINS_TOKEN]
          description: The operator to use for filtering. See [HubSpot API documentation](https://developers.hubspot.com/docs/api-reference/latest/crm/search-the-crm) for available operators.
        value:
          type: string
          example: "1234567890"
          description: The value to filter on.
        highValue:
          type: string
          example: "1234567899"
          description: The upper bound value to filter on. Only used with the `BETWEEN` operator.
        values:
          type: array
          items:
            type: string
          example: ["1234567890", "0987654321"]
          description: The values to filter on. Only used for `IN` and `NOT_IN` operators.

    BatchReadRequestBody:
      type: object
      required:
        - inputs
      properties:
        inputs:
          type: array
          items:
            $ref: '#/components/schemas/BatchReadInput'
        properties:
          type: array
          items:
            type: string
          description: The **StopWatch Time Log** properties that you'd like included in the response
          example:
            - a552213_record_id
            - hubspot_owner_id
            - a552213_category_text
        propertiesWithHistory:
          type: array
          items:
            type: string
          description: The **StopWatch Time Log** properties that you'd like included in the response, including a changelog history of their values
          example:
            - a552213_record_id
            - hubspot_owner_id
            - a552213_category_text
        idProperty:
          type: string
          example: a552213_record_id
          description: If omitted, defaults to `hs_object_id`. The only other property that can be used here for **StopWatch Time Logs** is `a552213_record_id`.

    SearchRequestBody:
      type: object
      properties:
        limit:
          type: integer
          example: 100
          description: The maximum number of records to return. Maximum is 200.
        after:
          type: string
          example: 100
          description: The paging cursor for fetching the next page of results. Supplied in the response as `paging.next.after`.
        properties:
          type: array
          items:
            type: string
          description: The **StopWatch Time Log** properties that you'd like included in the response
          example:
            - a552213_record_id
            - hubspot_owner_id
            - a552213_category_text
        query:
          type: string
          example: Meeting about Project XYZ
          description: A free text search query to apply to the search. See [HubSpot API documentation](https://developers.hubspot.com/docs/api-reference/latest/crm/search-the-crm) for more details on how this is applied.
        sorts:
          type: array
          items:
            type: object
            properties:
              propertyName:
                type: string
                example: hs_lastmodifieddate
                description: The property name to sort on. Must be a valid **StopWatch Time Log** property.
              direction:
                type: string
                example: DESCENDING
                enum: [ASCENDING, DESCENDING]
                description: The direction to sort results in.
        filterGroups:
          type: array
          items:
            $ref: '#/components/schemas/FilterGroup'
      example:
        limit: 100
        after: 100
        properties:
          - a552213_record_id
          - hubspot_owner_id
          - a552213_category_text
        sorts:
          - propertyName: hs_lastmodifieddate
            direction: DESCENDING
        filterGroups:
          - filters:
              - propertyName: a552213_category_text
                operator: CONTAINS_TOKEN
                value: Meetings

    CreateTimeLogRequestBody:
      type: object
      required:
        - properties
      properties:
        properties:
          type: object
          required:
            - a552213_record_id
            - a552213_assoc_obj_type
            - a552213_assoc_record_id
            - hubspot_owner_id
          properties:
            a552213_record_id:
              type: string
              example: Example Unique StopWatch Time Log ID - 1234567890
              description: Required. Must be unique. Converted to a string before creating the Time Log.
            a552213_assoc_obj_type:
              type: string
              enum: [contact, company, deal, ticket, appointment, course, listing, service, project, other]
              example: contact
              description: Required. If not `other`, the API attempts to create a default association to `a552213_assoc_record_id`.
            a552213_assoc_record_id:
              type: string
              example: "123456789"
              description: Required. HubSpot record ID for the associated CRM object.
            hubspot_owner_id:
              type: string
              example: "998877"
              description: Required. HubSpot Owner ID for the new Time Log. This usually represents the user who tracked the time. Retrieve valid HubSpot Owner IDs from the [List Owners](/apis/stopwatch/list-owners) endpoint.
          additionalProperties: true
          description: Properties must use exact, whole HubSpot property internal names. Retrieve valid property names from the [List Properties](/apis/stopwatch/list-properties) endpoint. Calculated and app-managed properties cannot be set.
      example:
        properties:
          a552213_record_id: Example Unique StopWatch Time Log ID - 1234567890
          a552213_assoc_obj_type: contact
          a552213_assoc_record_id: "123456789"
          hubspot_owner_id: "998877"
          a552213_description: Created via StopWatch API
          a552213_first_start_date: 2026-07-16T00:00:00.000Z
          a552213_first_end_date: 2026-07-16T01:00:00.000Z
          a552213_total_minutes: 60

    UpdateTimeLogRequestBody:
      type: object
      required:
        - properties
      properties:
        properties:
          type: object
          properties:
            a552213_assoc_obj_type:
              type: string
              enum: [contact, company, deal, ticket, appointment, course, listing, service, project, other]
              example: contact
              description: If is not `other` and is supplied with `a552213_assoc_record_id`, this endpoint attempts to create a default association.
            a552213_assoc_record_id:
              type: string
              example: "123456789"
              description: If supplied with `a552213_assoc_obj_type`, this endpoint attempts to create a default association.
          additionalProperties: true
          description: Properties must use exact, whole HubSpot property internal names. Retrieve valid property names from the [List Properties](/apis/stopwatch/list-properties) endpoint. Required properties cannot be unset or cleared. Calculated and app-managed properties cannot be updated.
      example:
        properties:
          a552213_assoc_obj_type: contact
          a552213_assoc_record_id: "123456789"
          a552213_description: Updated via StopWatch API

    AssociationStatus:
      type: object
      properties:
        attempted:
          type: boolean
          example: true
        status:
          type: string
          enum: [success, failed, skipped]
          example: success
        reason:
          type: string
          example: Both association properties were not supplied
        fromObjectType:
          type: string
          example: a552213_TIME_LOG
        fromRecordId:
          type: string
          example: "151993156822"
        toObjectType:
          type: string
          example: contacts
        toRecordId:
          type: string
          example: "123456789"
        statusCode:
          type: integer
          example: 200
        error:
          type: string
          example: <hubspot association error as string>

    TimelogWithAssociation:
      allOf:
        - $ref: '#/components/schemas/Timelog'
        - type: object
          properties:
            association:
              $ref: '#/components/schemas/AssociationStatus'

    Owner:
      type: object
      properties:
        id:
          type: string
          example: "998877"
          description: The HubSpot Owner ID. This is the value that should be used for `hubspot_owner_id` when creating a Time Log.
        email:
          type: string
          example: user@example.com
        firstName:
          type: string
          example: Example
        lastName:
          type: string
          example: User
        type:
          type: string
          example: PERSON
        createdAt:
          type: string
          format: date-time
          example: 2025-08-14T02:43:21.398Z
        updatedAt:
          type: string
          format: date-time
          example: 2025-08-14T02:43:21.398Z
        archived:
          type: boolean
          example: false

    OwnersResponse:
      type: object
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/Owner'
        paging:
          $ref: '#/components/schemas/Paging'

    BatchReadErrors:
      type: object
      properties:
        category:
          type: string
          example: "OBJECT_NOT_FOUND"
        context:
          type: object
          properties:
            ids:
              type: array
              items:
                type: string
                example: "12345"
        message:
          type: string
          example: "Could not get some null objects, they may be deleted or not exist. Check that ids are valid."
        status:
          type: string
          example: "error"

    BatchReadMultiStatusResponse:
      type: object
      properties:
        completedAt:
          type: string
          format: date-time
        errors:
          type: array
          items:
            $ref: '#/components/schemas/BatchReadErrors'
        numErrors:
          type: integer
        results:
          type: array
          items:
            $ref: '#/components/schemas/Timelog'
        startedAt:
          type: string
          format: date-time
        status:
          type: string
          example: COMPLETE

    Paging:
      type: object
      properties:
        next:
          type: object
          properties:
            after:
              type: string
              example: 278240677087
            link:
              type: string
              example: https://api.threadi.au/stopwatch/v1/timelogs?limit=1&properties=a552213_first_start_date%2Ca552213_total_years&after=278240677087

    Timelog:
      type: object
      properties:
        id:
          type: string
          example: 1234567890
        properties:
          type: object
          properties:
            a552213_record_id:
              type: string
              example: 998877 -- 2026-03-20 04:22:15.536
            a552213_assoc_obj_type:
              type: string
              enum: [contact, company, deal, ticket, appointment, course, listing, service, project, other]
            a552213_toggle:
              type: boolean
            a552213_assoc_record_id:
              type: string
              example: 9876543210
            a552213_first_start_date:
              type: string
              format: date-time
              example: 2026-02-27T21:24:18.038Z
            a552213_first_end_date:
              type: string
              format: date-time
              example: 2026-02-28T21:24:18.038Z
            a552213_last_start_date:
              type: string
              format: date-time
              example: 2026-02-27T21:24:18.038Z
            a552213_last_end_date:
              type: string
              format: date-time
              example: 2026-02-28T21:24:18.038Z
            a552213_total_minutes:
              type: number
              example: 1440
            a552213_total_hours:
              type: number
              example: 24
            a552213_total_days:
              type: number
              example: 1
            a552213_total_years:
              type: number
              example: 0.00273785
            a552213_total_decades:
              type: number
              example: 0.00027379
            a552213_total_centuries:
              type: number
              example: 0.00002738
            a552213_num_sessions:
              type: number
              example: 1
            a552213_week_year:
              type: string
              format: date
              example: 2026-02-23
            a552213_month_year:
              type: string
              format: date
              example: 2026-02-01
            a552213_is_split_week_record:
              type: boolean
            a552213_is_split_month_record:
              type: boolean
            a552213_workflow_managed_tag:
              type: string
              example: example_tag
            a552213_billable_minutes:
              type: number
              example: 0
            a552213_billable_hours:
              type: number
              example: 0
            a552213_billable_days:
              type: number
              example: 0
            a552213_billable_years:
              type: number
              example: 0
            a552213_billable_decades:
              type: number
              example: 0
            a552213_billable_centuries:
              type: number
              example: 0
            a552213_billable_amount:
              type: number
              example: 0
            a552213_cost_amount:
              type: number
              example: 0
            a552213_margin_amount:
              type: number
              example: 0
            a552213_is_one_time_log_per_session_record:
              type: boolean
            a552213_category_text:
              type: string
              example: Meetings
            a552213_description:
              type: string
              example: Meeting about Project XYZ
            a552213_billable_percentage:
              type: number
              example: 0
            a552213_service_name:
              type: string
              example: Training
            a552213_total_hours_auto:
              type: number
              example: 0
            a552213_total_days_auto:
              type: number
              example: 0
            a552213_total_years_auto:
              type: number
              example: 0
            a552213_total_decades_auto:
              type: number
              example: 0
            a552213_total_centuries_auto:
              type: number
              example: 0
            a552213_billable_hours_auto:
              type: number
              example: 0
            a552213_billable_days_auto:
              type: number
              example: 0
            a552213_billable_years_auto:
              type: number
              example: 0
            a552213_billable_decades_auto:
              type: number
              example: 0
            a552213_billable_centuries_auto:
              type: number
              example: 0
            a552213_billable_rate:
              type: number
              example: 0
            a552213_cost_rate:
              type: number
              example: 0
            a552213_billable_amount_auto:
              type: number
              example: 0
            a552213_cost_amount_auto:
              type: number
              example: 0
            a552213_margin_amount_auto:
              type: number
              example: 0
            a552213_title:
              type: string
              example: Training Session X
            a552213_created_via_stopwatch_api:
              type: string
              example: "true"
            a552213_last_updated_via_stopwatch_api:
              type: string
              example: "1784160000000"
          additionalProperties: true
        createdAt:
          type: string
          format: date-time
          example: 2026-02-27T21:24:18.038Z
        updatedAt:
          type: string
          format: date-time
          example: 2026-02-28T21:24:18.038Z
        archived:
          type: boolean

    Property:
      type: object
      properties:
        calculated:
          type: boolean
          example: false
        createdAt:
          type: string
          format: date-time
          example: 2025-06-26T00:54:50.551Z
        dataSensitivity:
          type: string
          enum: [non_sensitive, sensitive, highly_sensitive]
        description:
          type: string
          example: Unique record ID for StopWatch App ...
        displayOrder:
          type: number
          example: -1
        externalOptions:
          type: boolean
          example: false
        fieldType:
          type: string
          example: text
        formField:
          type: boolean
          example: false
        groupName:
          type: string
          example: time_log_information
        hasUniqueValue:
          type: boolean
          example: false
        hidden:
          type: boolean
          example: false
        label:
          type: string
          example: StopWatch Record ID
        modificationMetadata:
          type: object
          properties:
            archivable:
              type: boolean
              example: true
            readOnlyDefinition:
              type: boolean
              example: true
            readOnlyValue:
              type: boolean
              example: false
        name:
          type: string
          example: a552213_record_id
        options:
          type: array
          example: []
          items:
            type: object
        type:
          type: string
          example: string
        updatedAt:
          type: string
          format: date-time
          example: 2026-03-20T08:39:01.844Z

security:
  - bearerAuth: []

paths:
  /health:
    get:
      x-hidden: true
      summary: Health Check
      description: Check if the service is up and running.
      security: []
      responses:
        '200':
          description: Service is healthy.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
        '500':
          $ref: '#/components/responses/InternalServerError'

  /stopwatch/v1/timelogs:
    get:
      summary: List Time Logs
      description: |
        Retrieves a list of **StopWatch Time Log** records. Pagination links are 
        automatically rewritten to point to the **StopWatch API**.
        <Note>
          See [HubSpot API documentation](https://developers.hubspot.com/docs/api-reference/crm-objects-v3/basic/get-crm-v3-objects-objectType) for a list of available **Query Parameters**, including indicating target response `properties`
        </Note>
      responses:
        '200':
          description: A list of StopWatch Time Log records
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Timelog'
                  paging:
                    $ref: '#/components/schemas/Paging'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/TooManyRequestsError'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          $ref: '#/components/responses/ServiceUnavailableError'

    post:
      summary: Create Time Log
      description: |
        Creates a **StopWatch Time Log** record and attempts to set a corresponding HubSpot CRM association.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTimeLogRequestBody'
      responses:
        '201':
          description: Time Log created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TimelogWithAssociation'
              example:
                id: "151993156822"
                properties:
                  a552213_record_id: Example Unique StopWatch Time Log ID - 1234567890
                  a552213_assoc_obj_type: contact
                  a552213_assoc_record_id: "123456789"
                  hubspot_owner_id: "998877"
                  a552213_description: Created via StopWatch API
                  a552213_first_start_date: 2026-07-16T00:00:00.000Z
                  a552213_first_end_date: 2026-07-16T01:00:00.000Z
                  a552213_total_minutes: 60
                createdAt: 2026-07-16T00:00:00.000Z
                updatedAt: 2026-07-16T00:00:00.000Z
                archived: false
                association:
                  attempted: true
                  status: success
                  fromObjectType: a552213_TIME_LOG
                  fromRecordId: "151993156822"
                  toObjectType: contacts
                  toRecordId: "123456789"
                  statusCode: 200
        '207':
          description: Time Log created, but association creation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TimelogWithAssociation'
              example:
                id: "151993156822"
                properties:
                  a552213_record_id: Example Unique StopWatch Time Log ID - 1234567890
                  a552213_assoc_obj_type: contact
                  a552213_assoc_record_id: "123456789"
                  hubspot_owner_id: "998877"
                  a552213_description: Created via StopWatch API
                  a552213_first_start_date: 2026-07-16T00:00:00.000Z
                  a552213_first_end_date: 2026-07-16T01:00:00.000Z
                  a552213_total_minutes: 60
                createdAt: 2026-07-16T00:00:00.000Z
                updatedAt: 2026-07-16T00:00:00.000Z
                archived: false
                association:
                  attempted: true
                  status: failed
                  fromObjectType: a552213_TIME_LOG
                  fromRecordId: "151993156822"
                  toObjectType: contacts
                  toRecordId: "123456789"
                  statusCode: 404
                  error: HubSpot association target record was not found.
        '400':
          description: Invalid request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error400'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/TooManyRequestsError'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          $ref: '#/components/responses/ServiceUnavailableError'

  /stopwatch/v1/timelogs/{id}:
    get:
      summary: Get Time Log
      description: |
        Retrieves a single **StopWatch Time Log** record by ID.
        <Note>
          See [HubSpot API documentation](https://developers.hubspot.com/docs/api-reference/latest/crm/objects/objects/get-object) for the list of valid **Query Parameters**.
        </Note>
      parameters:
        - name: id
          in: path
          required: true
          description: The ID of the StopWatch Time Log record. By default, this is the HubSpot `hs_object_id`. If you want to retrieve a Time Log by its unique StopWatch Record Id, you can use the `idProperty` query parameter to specify `a552213_record_id` instead.
          schema:
            type: string
      responses:
        '200':
          description: Timelog details.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Timelog'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '429':
          $ref: '#/components/responses/TooManyRequestsError'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          $ref: '#/components/responses/ServiceUnavailableError'

    patch:
      summary: Update Time Log
      description: |
        Updates a **StopWatch Time Log** record by ID.
        <Note>
          See [HubSpot API documentation](https://developers.hubspot.com/docs/api-reference/latest/crm/objects/objects/update-object) for the list of valid **Query Parameters**.
        </Note>
      parameters:
        - name: id
          in: path
          required: true
          description: The ID of the StopWatch Time Log record. By default, this is the HubSpot `hs_object_id`. If you want to update a Time Log by its unique StopWatch Record Id, you can use the `idProperty` query parameter to specify `a552213_record_id` instead.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTimeLogRequestBody'
      responses:
        '200':
          description: Time Log updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TimelogWithAssociation'
        '207':
          description: Time Log updated, but association creation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TimelogWithAssociation'
        '400':
          description: Invalid request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error400'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
        '429':
          $ref: '#/components/responses/TooManyRequestsError'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          $ref: '#/components/responses/ServiceUnavailableError'

  /stopwatch/v1/timelogs/batch/read:
    post:
      summary: Batch Read Time Logs
      description: |
        Retrieves 1 or many **StopWatch Time Log** records by their ID(s).
        <Note>
          See [HubSpot API documentation](https://developers.hubspot.com/docs/api-reference/crm-objects-v3/batch/post-crm-v3-objects-objectType-batch-read) for the request **Body** structure and available **Query Parameters**
        </Note>
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchReadRequestBody'
      responses:
        '200':
          description: A list of StopWatch Time Log records where all requested records were found.
          content:
            application/json:
              schema:
                type: object
                properties:
                  completedAt:
                    type: string
                    format: date-time
                    example: '2026-03-25T06:01:32.819Z'
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Timelog'
                  startedAt:
                    type: string
                    format: date-time
                    example: '2026-03-25T06:01:32.757Z'
                  status:
                    type: string
                    example: COMPLETE
                  
        '207':
          $ref: '#/components/responses/MultiStatusResponse'
        '400':
          description: Invalid request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error400'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/TooManyRequestsError'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          $ref: '#/components/responses/ServiceUnavailableError'

  /stopwatch/v1/timelogs/properties:
    get:
      summary: List Properties
      description: |
        Retrieves all properties for the **StopWatch Time Log** object type.
        <Note>
          See [HubSpot API documentation](https://developers.hubspot.com/docs/api-reference/crm-properties-v3/core/get-crm-v3-properties-objectType) for a list of available **Query Parameters**
        </Note>
      responses:
        '200':
          description: A list of all StopWatch Time Log properties
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Property'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/TooManyRequestsError'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          $ref: '#/components/responses/ServiceUnavailableError'

  /stopwatch/v1/owners:
    get:
      summary: List Owners
      description: |
        Retrieves a list of HubSpot CRM Owners belonging to the authorised portal. Primarily supported so you can find valid `hubspot_owner_id` values for Time Log creation.
        <Note>
          See [HubSpot API documentation](https://developers.hubspot.com/docs/api-reference/latest/crm/owners/get-owners) for the list of valid **Query Parameters**.
        </Note>
      responses:
        '200':
          description: A page of HubSpot CRM Owners.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OwnersResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/TooManyRequestsError'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          $ref: '#/components/responses/ServiceUnavailableError'

  /stopwatch/v1/timelogs/search:
    post:
      summary: Search Time Logs
      description: |
        Search for **StopWatch Time Log** records using HubSpot's flexible **CRM Search** engine.
        <Note>
          See [HubSpot API documentation](https://developers.hubspot.com/docs/api-reference/latest/crm/search-the-crm) for the expected request **Body** structure
        </Note>
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SearchRequestBody'
      responses:
        '200':
          description: A page of StopWatch Time Log records matching the search criteria.
          content:
            application/json:
              schema:
                type: object
                properties:
                  paging:
                    type: object
                    properties:
                      next:
                        type: object
                        properties:
                          after:
                            type: string
                            example: 100
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/Timelog'
                  total:
                    type: integer
                    example: 123
        '400':
          description: Invalid request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error400'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/TooManyRequestsError'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          $ref: '#/components/responses/ServiceUnavailableError'
