openapi: 3.0.3
info:
  title: Zero API
  description: |
    The Zero API allows you to programmatically access and manage your workspaces, companies, contacts, deals, and custom activities.
    
    > **Beta Access:** The Zero API is currently in beta. Contact the Zero team to request access.
    
    ## Authentication
    All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)
  version: 1.12.0
  contact:
    name: Zero Support
    url: https://zero.inc/support

servers:
  - url: https://api.zero.inc
    description: Production server

security:
  - bearerAuth: []

tags:
  - name: Workspaces
    description: Manage workspaces
  - name: Companies
    description: Manage companies within workspaces
  - name: Contacts
    description: Manage contacts within workspaces
  - name: Deals
    description: Manage deals within workspaces
  - name: Notes
    description: Manage notes attached to companies, contacts, and deals.
  - name: Tasks
    description: Manage tasks. Tasks can be assigned to users and linked to companies, contacts, and deals.
  - name: Pipelines
    description: Manage sales pipelines and their stages. Deals are assigned to a pipeline and move through its stages.
  - name: Pipeline Stages
    description: Manage the stages within a pipeline. Each stage has a type, order, and optional confidence score.
  - name: Users
    description: Retrieve users accessible to the authenticated token. Users cannot be filtered by workspace — use the Memberships endpoint to find members of a specific workspace. Write operations on users are not exposed via the API.
  - name: Memberships
    description: Retrieve workspace memberships. A membership links a user to a workspace with a role (`admin` or `member`). Write operations on memberships are not exposed via the API.
  - name: Lists
    description: |
      Manage lists of contacts, companies, or deals. Lists come in two types:

      - **Regular lists** (`filterBased: false`) — records are explicitly added or removed by patching the `listIds` array on the contact/company/deal record.
      - **Dynamic lists** (`filterBased: true`) — records are not stored. The UI dynamically shows all workspace records that match the filters defined on the list's views. Dynamic list membership cannot be queried directly via the API — use a regular `GET /api/contacts` (or companies/deals) query with the same filter criteria instead.

      ### List + View pairing (required)
      A list without a view is not usable in the UI. Always create at least one view immediately after creating a list using `POST /api/views`. The first view should have `settings.isDefault: true` (or set it via a follow-up PATCH on the view). The UI sets `isDefault` automatically when the list is first opened.

      ### Icons
      Every list must have an `icon` set. A list without an icon will not display correctly in the UI. See the available icon names in the `icon` field description on `ListCreate`.

      ### Filter placement
      For dynamic lists, filters belong on the **views** (via `filters` on the View object), not on the list itself. The list-level `filters` field should always be left empty (`{}`). Setting filters on the list object has no effect on what records are shown.

      Each list can have multiple views, each with its own independent filter. The UI shows records matching each view's own filter when that view is active.
  - name: Views
    description: |
      Manage views within a list. Each view belongs to a list and defines its own filters, display type, and column configuration.

      ### View types
      - `grid` — flat table layout. Columns are defined in `settings.columns`.
      - `board` — kanban-style layout. Requires `settings.groupBy` to be set to the field to group by (e.g. `"stage"` or `"custom.<column-id>"`). To create a board view, create it as `grid` first, then PATCH with `{"type": "board"}`.

      ### Settings object
      ```json
      {
        "columns": [
          { "key": "&" },
          { "key": "value" },
          { "key": "closeDate" },
          { "key": "custom.<column-id>" }
        ],
        "groupBy": "stage",
        "isDefault": true
      }
      ```
      - `columns` — ordered list of visible columns. `"&"` is always the primary name column and should be first.
      - `groupBy` — field key to group rows by (board views require this; grid views support it optionally).
      - `isDefault` — marks this as the default view opened when navigating to the list. Only one view per list should have `isDefault: true`.

      ### Default columns
      The API has **no concept of default columns**. When a view is created without a `settings.columns` array, it defaults to `settings: {}` with no columns visible. The UI populates sensible defaults client-side, but these are not enforced or provided by the API. Always include a `columns` array when creating views via the API.

      ### Filters
      Each view has its own `filters` object. For dynamic lists, this is what determines which records appear. Filters use the same operator syntax as the `where` query parameter. Field keys map directly to entity field names.

      ```json
      {
        "stage": { "$in": ["<stage-uuid-1>", "<stage-uuid-2>"] },
        "lastActivity.time": { "$lt": "-7d" },
        "custom.<column-id>": "some-value"
      }
      ```

      Patching `filters` replaces the entire filters object — always include all desired filters, not just the changed ones.
  - name: Custom Activities
    description: |
      Log and manage custom activities (calls, meetings, and other interactions) linked to companies, contacts, and deals.
  - name: Custom Activity Types
    description: |
      Manage activity type definitions for a workspace. Each workspace comes with default types (Call, Meeting) and supports creating additional custom types.
  - name: Columns
    description: |
      Retrieve custom property definitions for a workspace. Use this to discover the IDs, types, and options of custom fields before filtering or updating records.
  - name: Files
    description: |
      Upload and manage files and attachments. Files are uploaded with `multipart/form-data` via `POST /api/files/upload` and can be linked to companies, contacts, deals, and notes.
  - name: Sequences
    description: |
      Build and control multi-step outreach sequences (cadences). A sequence is an ordered series of steps — emails, delays, tasks, and LinkedIn actions — that contacts move through automatically.

      ### Use the action endpoints, not raw CRUD
      Create, rename, clone, and delete sequences with the dedicated action endpoints below — **not** the generic `POST` / `PATCH` / `DELETE /api/sequences` verbs. Creating a sequence also provisions a backing **source list**, a start step, and a default view; deleting one cascades to its steps, list, and enrollments. The raw verbs skip this scaffolding and leave a sequence that cannot enroll contacts. (`PATCH /api/sequences/{sequenceId}` is still fine for editing settings like the sending mailbox — see that endpoint for the fields to avoid.)

      ### Lifecycle
      A sequence's `status` moves `draft` → `active` → `paused`. Use `/activate` and `/pause` to transition it — they also move in-flight contacts — rather than patching `status` directly.

      ### Steps
      Steps are managed with the regular `/api/sequenceSteps` CRUD endpoints. Each step has a `type` (`sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, `endSequence`) and an `order`. The `startSequence` step (order `0`) is created for you and should not be removed.
  - name: Sequence Enrollments
    description: |
      Enroll contacts into a sequence and track their progress.

      ### Enrolling a contact
      A sequence owns a **source list** (its `listId`). A contact is enrolled by adding that list to the contact's `listIds` array — there is no separate "enroll" endpoint:

      ```bash
      # 1. Find the sequence's source list id
      curl -G "https://api.zero.inc/api/sequences/SEQUENCE_UUID" \
        --data-urlencode "fields=id,name,listId" \
        -H "Authorization: Bearer YOUR_API_TOKEN"

      # 2. Add that list to the contact's listIds — this enrolls them.
      #    listIds is replaced wholesale, so include the contact's existing lists too.
      curl -X PATCH "https://api.zero.inc/api/contacts/CONTACT_UUID" \
        -H "Authorization: Bearer YOUR_API_TOKEN" \
        -H "Content-Type: application/json" \
        -d '{"listIds": ["EXISTING_LIST_UUID", "SEQUENCE_LIST_UUID"]}'
      ```

      The contact immediately gets an enrollment record (a `SequenceContactStatus`) with status `waitingForPickUp`. If the sequence is **active**, a background worker assigns the first step and begins sending within about a minute. Sequences whose start step uses manual roll-out land in `waitingForApproval` until approved in the app.

      ### Unenrolling
      Remove the sequence's list from the contact's `listIds`. The enrollment's status flips to `cancelled` and no further steps run.

      ### Tracking progress
      Use `GET /api/sequenceContactStatuses` for per-contact state, and `GET /api/sequences/progressSummary` for aggregate per-sequence metrics. `POST /api/sequences/reenroll` resets contacts to the start; `POST /api/sequences/forceNextStep` advances them.
  - name: Meetings
    description: |
      Read meetings (calendar events) and their transcripts. Only read (`GET`) access is exposed via the API — meetings and transcripts cannot be created, updated, or deleted.

      ### Transcripts are segment-grained
      A transcript is stored as many rows — one per spoken **segment** (a single speaker turn) — all sharing the same `calendarEventId`. There is no single "full transcript" object. To assemble a meeting's full transcript, list every segment for that meeting ordered by `startTime` and concatenate them:

      ```bash
      curl -G "https://api.zero.inc/api/calendarEventTranscripts" \
        --data-urlencode 'where={"calendarEventId":"CALENDAR_EVENT_UUID"}' \
        --data-urlencode 'orderBy={"startTime":"asc"}' \
        -H "Authorization: Bearer YOUR_API_TOKEN"
      ```

      A segment's `startTime`, `endTime`, and `duration` are **milliseconds from the start of the meeting**.

      ### Finding a meeting
      Transcript segments reference their meeting via `calendarEventId`. Use `GET /api/calendarEvents` to discover meetings (filter with `where` — e.g. by a `startTime` range, or by `contactIds`/`companyIds` using `$includes`), then query that meeting's transcript with the call above. Not every meeting has a transcript — only meetings recorded by a connected notetaker integration do.

paths:
  /api/lists:
    get:
      tags:
        - Lists
      summary: List lists
      description: |
        Returns lists in a workspace. Filter by workspace using `{"workspaceId": "<WORKSPACE_UUID>"}`.

        Use `filterBased` to distinguish between regular and dynamic lists:
        - `{"workspaceId": "<WS>", "filterBased": false}` — regular lists only
        - `{"workspaceId": "<WS>", "filterBased": true}` — dynamic lists only
      operationId: listLists
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/List'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Lists
      summary: Create a list
      description: |
        Create a new list. Set `filterBased: true` to create a dynamic list, or `filterBased: false` (default) for a regular list.

        **Always set an `icon`** — lists without an icon do not display correctly in the UI.

        After creating a list, immediately create at least one view with `POST /api/views` to make the list usable. A list without a view cannot be opened in the UI.
      operationId: createList
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ListCreate'
            examples:
              regular:
                summary: Regular list
                value:
                  workspaceId: "workspace-uuid"
                  name: "Key Accounts"
                  entity: "companies"
                  filterBased: false
                  color: "#2554F0"
                  icon: "list"
              dynamic:
                summary: Dynamic list
                value:
                  workspaceId: "workspace-uuid"
                  name: "All Trial Contacts"
                  entity: "contacts"
                  filterBased: true
                  color: "#24B770"
                  icon: "filter"
      responses:
        '200':
          description: List created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/List'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/lists/{listId}:
    get:
      tags:
        - Lists
      summary: Get a list
      description: Retrieve a single list by ID.
      operationId: getList
      parameters:
        - name: listId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/List'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags:
        - Lists
      summary: Update a list
      description: Update a list's metadata (name, color, icon, etc.). Returns only the changed fields.

        `entity` is immutable and cannot be changed after creation.
      operationId: updateList
      parameters:
        - name: listId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ListUpdate'
      responses:
        '200':
          description: List updated. Only changed fields are returned in `data`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
    delete:
      tags:
        - Lists
      summary: Delete a list
      description: |
        Delete a list. By default performs a hard delete (`data: 1`). Pass `?archive=true` for a soft delete.

        > **Note:** Deleting a list does not delete the records within it. For regular lists, the records simply lose their association with the list.
      operationId: deleteList
      parameters:
        - name: listId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: archive
          in: query
          schema:
            type: boolean
            default: false
          description: If true, soft-deletes (archives) the list instead of permanently deleting it
      responses:
        '200':
          description: Deletion result
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    oneOf:
                      - type: integer
                        description: "`1` on hard delete success, `0` if not found"
                      - type: array
                        description: Archived list object(s) on soft delete
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/views:
    get:
      tags:
        - Views
      summary: List views
      description: |
        Returns views. Filter by list using `{"listId": "<LIST_UUID>"}` or by workspace using `{"workspaceId": "<WORKSPACE_UUID>"}`.

        Each view has its own `filters` object that determines which records are shown. For dynamic lists, this is the primary mechanism for filtering records.

        ### Filter syntax

        View filters use the same operator syntax as the `where` parameter across the API. Field keys map to entity field names:

        ```json
        {
          "name": { "$contains": "acme" },
          "lastActivity.time": { "$lt": "-7d" },
          "title": { "$contains": "Engineer" },
          "custom.<column-id>": true
        }
        ```
      operationId: listViews
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/View'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Views
      summary: Create a view
      description: |
        Create a new view within a list. Both `entity` and `type` are required.

        Supported `type` values: `grid`, `board`.

        > **Board views:** Create as `grid` first, then PATCH `{"type": "board"}`. Board views should have `settings.groupBy` set — the API does not enforce it but the UI needs it to display columns correctly.

        > **Default view:** Set `settings.isDefault: true` on the first view, or apply it via a follow-up PATCH. Only one view per list should be the default.

        Filters can be set at creation or added later via PATCH. For dynamic lists, filters on the view determine which records are shown.
      operationId: createView
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ViewCreate'
            example:
              workspaceId: "workspace-uuid"
              listId: "list-uuid"
              name: "Active contacts"
              entity: "contacts"
              type: "grid"
              filters:
                title:
                  $contains: "Engineer"
                lastActivity.time:
                  $lt: "-7d"
      responses:
        '200':
          description: View created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/View'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/views/{viewId}:
    get:
      tags:
        - Views
      summary: Get a view
      description: Retrieve a single view by ID.
      operationId: getView
      parameters:
        - name: viewId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/View'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags:
        - Views
      summary: Update a view
      description: |
        Update a view's name, type, filters, or display settings. Returns only changed fields.

        `entity` is immutable and cannot be changed after creation.

        Patching `filters` replaces the entire filters object — include all desired filters, not just the changed ones.

        Patching `settings` replaces the entire settings object — always include the full `columns` array and `groupBy` value, not just the changed fields.

        To convert a grid view to a board view: `PATCH {"type": "board"}`. Board views should have `settings.groupBy` set — the API does not enforce it but the UI needs it to display columns correctly.
      operationId: updateView
      parameters:
        - name: viewId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ViewUpdate'
            example:
              filters:
                title:
                  $contains: "Engineer"
                lastActivity.time:
                  $lt: "-14d"
      responses:
        '200':
          description: View updated. Only changed fields are returned in `data`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
    delete:
      tags:
        - Views
      summary: Delete a view
      description: |
        Permanently delete a view. Returns `{"data": 1}` on success, `{"data": 0}` if not found.
      operationId: deleteView
      parameters:
        - name: viewId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Deletion result
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: integer
                    description: "`1` if deleted, `0` if not found"
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'


  /api/companies:
    get:
      tags:
        - Companies
      summary: List companies
      description: |
        Returns companies the user has access to. Filter by workspace using the `where` parameter: `{"workspaceId": "<WORKSPACE_UUID>"}`.
      operationId: listCompanies
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Company'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Companies
      summary: Create a company
      description: Create a new company in a workspace.
      operationId: createCompany
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CompanyCreate'
            example:
              workspaceId: "workspace-uuid"
              name: "Acme Inc"
              domain: "acme.com"
              linkedin: "acme-inc"
      responses:
        '200':
          description: Company created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Company'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/companies/{companyId}:
    get:
      tags:
        - Companies
      summary: Get a company
      description: Returns a single company by ID.
      operationId: getCompany
      parameters:
        - name: companyId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: |
            Successful response. If the company does not exist, the API returns HTTP 200 with an empty body (`{}`).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Company'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags:
        - Companies
      summary: Update a company
      description: |
        Update an existing company.
        
        ### Updating custom properties
        
        > **⚠️ Important:** Do **not** pass the entire `custom` object when updating custom properties — this will overwrite all existing custom property values on the record.
        
        Instead, use **dot-notation** to update individual custom properties:
        
        ```json
        // ✅ Correct — only updates the specific custom property
        {"custom.54e1ca7d-69c3-4b77-8266-8085b5834116": "hello world"}
        
        // ❌ Wrong — overwrites the entire custom object, deleting all other values
        {"custom": {"54e1ca7d-69c3-4b77-8266-8085b5834116": "hello world"}}
        ```
        
        Use `GET /api/columns` to discover the available custom property IDs and types for the workspace.
      operationId: updateCompany
      parameters:
        - name: companyId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CompanyUpdate'
            example:
              name: "Acme Corporation"
      responses:
        '200':
          description: Only the changed fields are returned in `data` (e.g. `name`, `updatedAt`, `updatedById`). Use `GET` on the record to retrieve the full updated object.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    description: Only the changed fields, not the full object.
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
        # Note: If the company does not exist, the API returns HTTP 200 with {"sideEffects": []} rather than a 404.
    delete:
      tags:
        - Companies
      summary: Delete a company
      description: Delete a company. Use archive=true for soft delete.
      operationId: deleteCompany
      parameters:
        - name: companyId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: archive
          in: query
          description: If true, soft deletes (archives) the company instead of permanently deleting it.
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Company deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    description: |
                      Hard delete (`archive` omitted or `false`): returns `1` on success.
                      Soft delete (`archive=true`): returns a single-element array containing the archived company object.
                    oneOf:
                      - type: integer
                        example: 1
                      - type: array
                        items:
                          $ref: '#/components/schemas/Company'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/contacts:
    get:
      tags:
        - Contacts
      summary: List contacts
      description: |
        Returns contacts the user has access to. Filter by workspace using the `where` parameter: `{"workspaceId": "<WORKSPACE_UUID>"}`.
      operationId: listContacts
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Contact'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Contacts
      summary: Create a contact
      description: |
        Create a new contact in a workspace.
        
        Note: If companyId is not provided but email or linkedin is, the system will automatically find or create the associated company.
      operationId: createContact
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactCreate'
            example:
              workspaceId: "workspace-uuid"
              name: "John Doe"
              email: "john@acme.com"
              title: "CEO"
              companyId: "company-uuid"
              linkedin: "johndoe"
      responses:
        '200':
          description: Contact created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Contact'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/contacts/{contactId}:
    get:
      tags:
        - Contacts
      summary: Get a contact
      description: Returns a single contact by ID.
      operationId: getContact
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: |
            Successful response. If the contact does not exist, the API returns HTTP 200 with an empty body (`{}`).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Contact'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags:
        - Contacts
      summary: Update a contact
      description: |
        Update an existing contact.
        
        ### Updating custom properties
        
        > **⚠️ Important:** Do **not** pass the entire `custom` object when updating custom properties — this will overwrite all existing custom property values on the record.
        
        Instead, use **dot-notation** to update individual custom properties:
        
        ```json
        // ✅ Correct — only updates the specific custom property
        {"custom.54e1ca7d-69c3-4b77-8266-8085b5834116": "hello world"}
        
        // ❌ Wrong — overwrites the entire custom object, deleting all other values
        {"custom": {"54e1ca7d-69c3-4b77-8266-8085b5834116": "hello world"}}
        ```
        
        Use `GET /api/columns` to discover the available custom property IDs and types for the workspace.
      operationId: updateContact
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactUpdate'
      responses:
        '200':
          description: Only the changed fields are returned in `data` (e.g. `name`, `updatedAt`, `updatedById`). Use `GET` on the record to retrieve the full updated object.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    description: Only the changed fields, not the full object.
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
        # Note: If the contact does not exist, the API returns HTTP 200 with {"sideEffects": []} rather than a 404.
    delete:
      tags:
        - Contacts
      summary: Delete a contact
      description: Delete a contact. Use archive=true for soft delete.
      operationId: deleteContact
      parameters:
        - name: contactId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: archive
          in: query
          description: If true, soft deletes (archives) the contact instead of permanently deleting it.
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Contact deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    description: |
                      Hard delete (`archive` omitted or `false`): returns `1` on success.
                      Soft delete (`archive=true`): returns a single-element array containing the archived contact object.
                    oneOf:
                      - type: integer
                        example: 1
                      - type: array
                        items:
                          $ref: '#/components/schemas/Contact'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/deals:
    get:
      tags:
        - Deals
      summary: List deals
      description: |
        Returns deals the user has access to. Filter by workspace using the `where` parameter: `{"workspaceId": "<WORKSPACE_UUID>"}`.
      operationId: listDeals
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Deal'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Deals
      summary: Create a deal
      description: |
        Create a new deal in a workspace.
        
        Note: If stage is not provided, it will default to the workspace's defaultDealStage setting.
      operationId: createDeal
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DealCreate'
            example:
              workspaceId: "workspace-uuid"
              name: "Enterprise Deal"
              companyId: "company-uuid"
              contactIds:
                - "contact-uuid-1"
                - "contact-uuid-2"
              value: 50000
              closeDate: "2024-06-30"
      responses:
        '200':
          description: Deal created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Deal'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/deals/{dealId}:
    get:
      tags:
        - Deals
      summary: Get a deal
      description: Returns a single deal by ID.
      operationId: getDeal
      parameters:
        - name: dealId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: |
            Successful response. If the deal does not exist, the API returns HTTP 200 with an empty body (`{}`).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Deal'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags:
        - Deals
      summary: Update a deal
      description: |
        Update an existing deal.
        
        ### Updating custom properties
        
        > **⚠️ Important:** Do **not** pass the entire `custom` object when updating custom properties — this will overwrite all existing custom property values on the record.
        
        Instead, use **dot-notation** to update individual custom properties:
        
        ```json
        // ✅ Correct — only updates the specific custom property
        {"custom.54e1ca7d-69c3-4b77-8266-8085b5834116": "hello world"}
        
        // ❌ Wrong — overwrites the entire custom object, deleting all other values
        {"custom": {"54e1ca7d-69c3-4b77-8266-8085b5834116": "hello world"}}
        ```
        
        Use `GET /api/columns` to discover the available custom property IDs and types for the workspace.
      operationId: updateDeal
      parameters:
        - name: dealId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DealUpdate'
      responses:
        '200':
          description: Only the changed fields are returned in `data` (e.g. `name`, `updatedAt`, `updatedById`). Use `GET` on the record to retrieve the full updated object.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    description: Only the changed fields, not the full object.
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
        # Note: If the deal does not exist, the API returns HTTP 200 with {"sideEffects": []} rather than a 404.
    delete:
      tags:
        - Deals
      summary: Delete a deal
      description: Delete a deal. Use archive=true for soft delete.
      operationId: deleteDeal
      parameters:
        - name: dealId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: archive
          in: query
          description: If true, soft deletes (archives) the deal instead of permanently deleting it.
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Deal deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    description: |
                      Hard delete (`archive` omitted or `false`): returns `1` on success.
                      Soft delete (`archive=true`): returns a single-element array containing the archived deal object.
                    oneOf:
                      - type: integer
                        example: 1
                      - type: array
                        items:
                          $ref: '#/components/schemas/Deal'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/customActivities:
    get:
      tags:
        - Custom Activities
      summary: List custom activities
      description: |
        Returns custom activities the user has access to. Filter by workspace using the `where` parameter: `{"workspaceId": "<WORKSPACE_UUID>"}`.

        Common filters:
        - Activities for a contact: `{"workspaceId": "<WS>", "contactIds": {"$includes": "<CONTACT_UUID>"}}`
        - Activities by type: `{"workspaceId": "<WS>", "typeId": "<TYPE_UUID>"}`
        - Activities in a date range: `{"workspaceId": "<WS>", "time": {"$gte": "2026-01-01", "$lte": "2026-03-31"}}`
      operationId: listCustomActivities
      parameters:
        - $ref: "#/components/parameters/fields"
        - $ref: "#/components/parameters/where"
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
        - $ref: "#/components/parameters/orderBy"
      responses:
        "200":
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/CustomActivity"
                  total:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags:
        - Custom Activities
      summary: Create a custom activity
      description: |
        Create a new custom activity in a workspace.

        Activities can be linked to multiple companies, contacts, and deals simultaneously via `companyIds`, `contactIds`, and `dealIds`.

        Use `typeId` to associate the activity with a custom activity type (e.g. Call, Meeting). Use `GET /api/customActivityTypes` to discover available types.

        The `content` field is a flexible JSONB object for storing structured data about the activity (e.g. call notes, meeting agenda). The `custom` field stores custom property values defined on the activity type's `customFields`.
      operationId: createCustomActivity
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CustomActivityCreate"
            example:
              workspaceId: "workspace-uuid"
              name: "Discovery call with Acme"
              typeId: "call-type-uuid"
              time: "2026-03-13T14:00:00.000Z"
              contactIds: ["contact-uuid"]
              companyIds: ["company-uuid"]
              dealIds: ["deal-uuid"]
              content:
                notes: "Discussed pricing and timeline."
              custom:
                call-result: "connected"
      responses:
        "200":
          description: Custom activity created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/CustomActivity"
                  sideEffects:
                    type: array
                    items: {}
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/customActivities/{customActivityId}:
    get:
      tags:
        - Custom Activities
      summary: Get a custom activity
      description: |
        Returns a single custom activity by ID.

        If the activity does not exist, the API returns HTTP 200 with an empty body (`{}`).
      operationId: getCustomActivity
      parameters:
        - name: customActivityId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/CustomActivity"
        "401":
          $ref: "#/components/responses/Unauthorized"
    patch:
      tags:
        - Custom Activities
      summary: Update a custom activity
      description: |
        Update an existing custom activity.

        > **⚠️ Important:** Array fields (`contactIds`, `companyIds`, `dealIds`, `userIds`) are **replaced** on PATCH, not merged. Always include all desired IDs.

        ### Updating custom properties

        > **⚠️ Important:** Do **not** pass the entire `custom` object when updating custom properties — this will overwrite all existing custom property values on the record.

        Instead, use **dot-notation** to update individual custom properties:

        ```json
        // ✅ Correct — only updates the specific custom property
        {"custom.call-result": "connected"}

        // ❌ Wrong — overwrites the entire custom object
        {"custom": {"call-result": "connected"}}
        ```
      operationId: updateCustomActivity
      parameters:
        - name: customActivityId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CustomActivityUpdate"
      responses:
        "200":
          description: Custom activity updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/CustomActivity"
                  sideEffects:
                    type: array
                    items: {}
        "401":
          $ref: "#/components/responses/Unauthorized"
    delete:
      tags:
        - Custom Activities
      summary: Delete a custom activity
      description: Delete a custom activity. Use `archive=true` for soft delete.
      operationId: deleteCustomActivity
      parameters:
        - name: customActivityId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: archive
          in: query
          description: If true, soft deletes (archives) the activity instead of permanently deleting it.
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: Custom activity deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    description: |
                      Hard delete (`archive` omitted or `false`): returns `1` on success.
                      Soft delete (`archive=true`): returns a single-element array containing the archived activity object.
                    oneOf:
                      - type: integer
                        example: 1
                      - type: array
                        items:
                          $ref: "#/components/schemas/CustomActivity"
                  sideEffects:
                    type: array
                    items: {}
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/customActivityTypes:
    get:
      tags:
        - Custom Activity Types
      summary: List custom activity types
      description: |
        Returns custom activity type definitions for a workspace. Filter by workspace using the `where` parameter: `{"workspaceId": "<WORKSPACE_UUID>"}`.

        Each workspace comes with default types (Call, Meeting). Use this endpoint to discover available types and their custom fields before creating activities.
      operationId: listCustomActivityTypes
      parameters:
        - $ref: "#/components/parameters/fields"
        - $ref: "#/components/parameters/where"
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/offset"
        - $ref: "#/components/parameters/orderBy"
      responses:
        "200":
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/CustomActivityType"
                  total:
                    type: integer
              example:
                data:
                  - id: "type-uuid-1"
                    workspaceId: "workspace-uuid"
                    name: "Call"
                    icon: "phone"
                    color: "#C48E1C"
                    shortcut: "K"
                    description: "A phone call"
                    customFields:
                      - id: "call-result"
                        name: "Call result"
                        type: "select"
                        options:
                          - key: "connected"
                            name: "Connected"
                            color: "#4CAF50"
                          - key: "busy"
                            name: "Busy"
                            color: "#FF9800"
                          - key: "no-answer"
                            name: "No answer"
                            color: "#9E9E9E"
                          - key: "left-voicemail"
                            name: "Left voicemail"
                            color: "#2196F3"
                          - key: "left-live-message"
                            name: "Left live message"
                            color: "#00BCD4"
                          - key: "wrong-number"
                            name: "Wrong number"
                            color: "#F44336"
                  - id: "type-uuid-2"
                    workspaceId: "workspace-uuid"
                    name: "Meeting"
                    icon: "calendar"
                    color: "#7B1FA2"
                    shortcut: "M"
                    description: "Notes from a meeting"
                    customFields: []
                total: 2
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags:
        - Custom Activity Types
      summary: Create a custom activity type
      description: |
        Create a new activity type definition in a workspace. Activity types define the categories of activities users can log (e.g. Call, Meeting, Site Visit).

        Each type can have custom fields that appear when logging an activity of that type.
      operationId: createCustomActivityType
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CustomActivityTypeCreate"
            example:
              workspaceId: "workspace-uuid"
              name: "Site Visit"
              icon: "building"
              color: "#4CAF50"
              shortcut: "V"
              description: "An in-person site visit"
              customFields:
                - id: "visit-outcome"
                  name: "Visit outcome"
                  type: "select"
                  options:
                    - key: "positive"
                      name: "Positive"
                      color: "#4CAF50"
                    - key: "neutral"
                      name: "Neutral"
                      color: "#FF9800"
                    - key: "negative"
                      name: "Negative"
                      color: "#F44336"
      responses:
        "200":
          description: Custom activity type created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/CustomActivityType"
                  sideEffects:
                    type: array
                    items: {}
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/customActivityTypes/{customActivityTypeId}:
    get:
      tags:
        - Custom Activity Types
      summary: Get a custom activity type
      description: |
        Returns a single custom activity type by ID.

        If the type does not exist, the API returns HTTP 200 with an empty body (`{}`).
      operationId: getCustomActivityType
      parameters:
        - name: customActivityTypeId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/CustomActivityType"
        "401":
          $ref: "#/components/responses/Unauthorized"
    patch:
      tags:
        - Custom Activity Types
      summary: Update a custom activity type
      description: |
        Update an existing custom activity type.

        > **⚠️ Important:** The `customFields` array is **replaced** entirely on update, not merged. Always include all desired custom fields when patching.
      operationId: updateCustomActivityType
      parameters:
        - name: customActivityTypeId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CustomActivityTypeUpdate"
      responses:
        "200":
          description: Custom activity type updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/CustomActivityType"
                  sideEffects:
                    type: array
                    items: {}
        "401":
          $ref: "#/components/responses/Unauthorized"
    delete:
      tags:
        - Custom Activity Types
      summary: Delete a custom activity type
      description: Delete a custom activity type. Use `archive=true` for soft delete.
      operationId: deleteCustomActivityType
      parameters:
        - name: customActivityTypeId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: archive
          in: query
          description: If true, soft deletes (archives) the type instead of permanently deleting it.
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: Custom activity type deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    description: |
                      Hard delete (`archive` omitted or `false`): returns `1` on success.
                      Soft delete (`archive=true`): returns a single-element array containing the archived type object.
                    oneOf:
                      - type: integer
                        example: 1
                      - type: array
                        items:
                          $ref: "#/components/schemas/CustomActivityType"
                  sideEffects:
                    type: array
                    items: {}
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/columns:
    get:
      tags:
        - Columns
      summary: List custom property definitions
      description: |
        Returns the custom property (column) definitions for a workspace. Filter by workspace using the `where` parameter: `{"workspaceId": "<WORKSPACE_UUID>"}`. Strongly recommended — omitting it returns columns across all workspaces the user has access to.

        Use this endpoint to discover the IDs, types, and available options of custom fields before filtering or updating records.
        
        Custom properties are stored in the `custom` object on companies, contacts, and deals. Each custom property is keyed by its UUID (`id` from this response).
        
        For example, if this endpoint returns a column with `"id": "54e1ca7d-69c3-4b77-8266-8085b5834116"` and `"type": "select"`, you can:
        - **Filter** records using: `{"custom.54e1ca7d-69c3-4b77-8266-8085b5834116": {"$in": ["<OPTION_KEY_UUID>", "<OPTION_KEY_UUID>"]}}`
        - **Update** a record using: `{"custom.54e1ca7d-69c3-4b77-8266-8085b5834116": "<OPTION_KEY_UUID>"}`

        > **Note:** For `select` and `multiselect` columns, option values are UUIDs stored in the `key` field of each option object. Use the `key` UUID (not the human-readable `name`) when filtering or updating records.
      operationId: listColumns
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Column'
                  total:
                    type: integer
                    description: Total number of custom property definitions
              example:
                data:
                  - id: "54e1ca7d-69c3-4b77-8266-8085b5834116"
                    workspaceId: "dd66c03b-1a84-4c9d-b118-3cc54e21d019"
                    name: "Industry"
                    type: "select"
                    entity: "companies"
                    options:
                      - key: "3e839b5c-b311-4887-b2da-727d2d75cdd6"
                        name: "SaaS"
                        color: "#4CAF50"
                      - key: "7f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c"
                        name: "Fintech"
                        color: "#2196F3"
                  - id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    workspaceId: "dd66c03b-1a84-4c9d-b118-3cc54e21d019"
                    name: "ARR"
                    type: "currency"
                    entity: "companies"
                    options: null
                  - id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
                    workspaceId: "dd66c03b-1a84-4c9d-b118-3cc54e21d019"
                    name: "Tags"
                    type: "multiselect"
                    entity: "deals"
                    options:
                      - key: "a1b1c1d1-e1f1-1111-aaaa-111111111111"
                        name: "Enterprise"
                        color: "#FF9800"
                      - key: "b2b2c2d2-e2f2-2222-bbbb-222222222222"
                        name: "Strategic"
                        color: "#9C27B0"
                total: 3
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/notes:
    get:
      tags:
        - Notes
      summary: List notes
      description: |
        Returns notes the user has access to. Filter by workspace using the `where` parameter: `{"workspaceId": "<WORKSPACE_UUID>"}`.

        Notes can also be filtered by `contactId`, `companyId`, or `dealId` to retrieve notes for a specific record.
      operationId: listNotes
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Note'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Notes
      summary: Create a note
      description: |
        Create a new note in a workspace. Notes can be associated with a company, contact, or deal (or any combination).

        The `content` field uses the [Tiptap](https://tiptap.dev/) document format — a JSON structure with a `type: "doc"` root node containing an array of block nodes (paragraphs, headings, etc.).

        ### Minimal Tiptap document

        An empty note body:
        ```json
        {"type": "doc", "content": []}
        ```

        ### Tiptap document with text

        ```json
        {
          "type": "doc",
          "content": [
            {
              "type": "paragraph",
              "content": [
                {"type": "text", "text": "Your note text here"}
              ]
            }
          ]
        }
        ```

        The `content` field can be omitted on creation and set later via `PATCH`.
      operationId: createNote
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NoteCreate'
            example:
              workspaceId: "workspace-uuid"
              name: "Meeting notes"
              emoji: "🗒️"
              contactId: "contact-uuid"
              companyId: "company-uuid"
              content:
                type: "doc"
                content:
                  - type: "paragraph"
                    content:
                      - type: "text"
                        text: "Discussed Q2 roadmap and pricing."
      responses:
        '200':
          description: Note created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Note'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/notes/{noteId}:
    get:
      tags:
        - Notes
      summary: Get a note
      description: Retrieve a single note by ID.
      operationId: getNote
      parameters:
        - name: noteId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Note'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags:
        - Notes
      summary: Update a note
      description: |
        Update an existing note.

        > **Note:** Unlike other PATCH endpoints, the response only returns the fields that were changed — not the full note object.

        To update the note body, pass a complete Tiptap document in `content` — this replaces the existing content entirely.
      operationId: updateNote
      parameters:
        - name: noteId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NoteUpdate'
            example:
              name: "Updated meeting notes"
              emoji: "🔥"
              content:
                type: "doc"
                content:
                  - type: "paragraph"
                    content:
                      - type: "text"
                        text: "Updated note content."
      responses:
        '200':
          description: |
            Note updated successfully. Only the fields that were changed are returned in `data` (not the full note object).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    description: Partial note object containing only the updated fields plus `updatedAt` and `updatedById`.
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
    delete:
      tags:
        - Notes
      summary: Delete a note
      description: Delete a note. Use `archive=true` for soft delete (recoverable), or omit for permanent deletion.
      operationId: deleteNote
      parameters:
        - name: noteId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: archive
          in: query
          description: If true, soft deletes (archives) the note instead of permanently deleting it.
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Note deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    description: |
                      Hard delete (`archive` omitted or `false`): returns `1` on success.
                      Soft delete (`archive=true`): returns a single-element array containing the archived note object.
                    oneOf:
                      - type: integer
                        example: 1
                      - type: array
                        items:
                          $ref: '#/components/schemas/Note'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'


  /api/calendarEvents:
    get:
      tags:
        - Meetings
      summary: List meetings
      description: |
        Returns meetings (calendar events) the user has access to. Filter by workspace using the `where` parameter: `{"workspaceId": "<WORKSPACE_UUID>"}`.

        Because `contactIds`, `companyIds`, and `dealIds` are arrays, filter by a linked record using the `$includes` operator — e.g. `{"contactIds": {"$includes": "<CONTACT_UUID>"}}`. Filter by date window with a `startTime` range, e.g. `{"startTime": {"$gte": "2026-06-01T00:00:00Z", "$lt": "2026-07-01T00:00:00Z"}}`.

        Only read access is exposed — meetings cannot be created, updated, or deleted via the API.
      operationId: listCalendarEvents
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CalendarEvent'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/calendarEvents/{calendarEventId}:
    get:
      tags:
        - Meetings
      summary: Get a meeting
      description: Retrieve a single meeting (calendar event) by ID.
      operationId: getCalendarEvent
      parameters:
        - name: calendarEventId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CalendarEvent'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/calendarEventTranscripts:
    get:
      tags:
        - Meetings
      summary: List transcript segments
      description: |
        Returns meeting transcript segments the user has access to. **Each row is a single transcript segment** (one speaker turn), so a meeting's full transcript spans many rows that share the same `calendarEventId`.

        Filter to one meeting with the `where` parameter and order by `startTime` to reconstruct the conversation in order:

        ```bash
        curl -G "https://api.zero.inc/api/calendarEventTranscripts" \
          --data-urlencode 'where={"calendarEventId":"CALENDAR_EVENT_UUID"}' \
          --data-urlencode 'orderBy={"startTime":"asc"}' \
          -H "Authorization: Bearer YOUR_API_TOKEN"
        ```

        A segment's `startTime`, `endTime`, and `duration` are milliseconds from the start of the meeting. Only read access is exposed — transcripts cannot be created, updated, or deleted via the API.
      operationId: listCalendarEventTranscripts
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Transcript'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/calendarEventTranscripts/{transcriptId}:
    get:
      tags:
        - Meetings
      summary: Get a transcript segment
      description: |
        Retrieve a single transcript segment by ID. A segment is one speaker turn — not a meeting's full transcript. To get the full transcript, list segments filtered by `calendarEventId` (see **List transcript segments**).
      operationId: getCalendarEventTranscript
      parameters:
        - name: transcriptId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Transcript'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/workspaces:
    get:
      tags:
        - Workspaces
      summary: List workspaces
      description: |
        Returns all workspaces the authenticated user has access to. No `workspaceId` filter is needed — results are automatically scoped to the authenticated user's memberships.

        The `settings` field contains workspace configuration including timezone, currency, contact types, and column display configuration. The `domains` field lists the email domains associated with the workspace.
      operationId: listWorkspaces
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Workspace'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/workspaces/{workspaceId}:
    get:
      tags:
        - Workspaces
      summary: Get a workspace
      description: Retrieve a single workspace by ID.
      operationId: getWorkspace
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Workspace'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags:
        - Workspaces
      summary: Update a workspace
      description: |
        Update workspace properties. Returns only the changed fields.

        > **Note:** `settings` is a complex nested object. When patching `settings`, the entire object is replaced — include all desired settings, not just the changed fields.
      operationId: updateWorkspace
      parameters:
        - name: workspaceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WorkspaceUpdate'
      responses:
        '200':
          description: Workspace updated. Only changed fields are returned in `data`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/users:
    get:
      tags:
        - Users
      summary: List users
      description: |
        Returns users accessible to the authenticated token.

        > **Note:** Users cannot be filtered by `workspaceId`. To retrieve members of a specific workspace, use `GET /api/memberships?where={"workspaceId":"<WS>"}` to get the `userId` values, then look them up with `GET /api/users?where={"id":{"$in":[...]}}`.
      operationId: listUsers
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/users/{userId}:
    get:
      tags:
        - Users
      summary: Get a user
      description: Retrieve a single user by ID.
      operationId: getUser
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/User'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/memberships:
    get:
      tags:
        - Memberships
      summary: List memberships
      description: |
        Returns memberships the user has access to. Filter by workspace using `{"workspaceId": "<WORKSPACE_UUID>"}`.

        ### Resolving workspace members

        The recommended pattern for finding all users in a workspace:

        **Step 1** — Get membership records:
        ```
        GET /api/memberships?where={"workspaceId":"<WS_UUID>"}
        ```

        **Step 2** — Resolve user details using the returned `userId` values:
        ```
        GET /api/users?where={"id":{"$in":["<userId1>","<userId2>",...]}}
        ```
      operationId: listMemberships
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Membership'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/memberships/{membershipId}:
    get:
      tags:
        - Memberships
      summary: Get a membership
      description: Retrieve a single membership by ID.
      operationId: getMembership
      parameters:
        - name: membershipId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Membership'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/pipelines:
    get:
      tags:
        - Pipelines
      summary: List pipelines
      description: |
        Returns pipelines the user has access to. Filter by workspace using the `where` parameter: `{"workspaceId": "<WORKSPACE_UUID>"}`.

        > **Note:** Use `GET /api/pipelineStages` to retrieve the stages within each pipeline. The pipeline object itself does not embed its stages.
      operationId: listPipelines
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Pipeline'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Pipelines
      summary: Create a pipeline
      description: Create a new pipeline in a workspace.
      operationId: createPipeline
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PipelineCreate'
            example:
              workspaceId: "workspace-uuid"
              name: "Sales Pipeline"
              description: "Main outbound sales pipeline"
              confidenceEnabled: true
      responses:
        '200':
          description: Pipeline created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Pipeline'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/pipelines/{pipelineId}:
    get:
      tags:
        - Pipelines
      summary: Get a pipeline
      description: Retrieve a single pipeline by ID.
      operationId: getPipeline
      parameters:
        - name: pipelineId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Pipeline'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags:
        - Pipelines
      summary: Update a pipeline
      description: |
        Update an existing pipeline. Returns only the changed fields.
      operationId: updatePipeline
      parameters:
        - name: pipelineId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PipelineUpdate'
      responses:
        '200':
          description: Pipeline updated. Only changed fields are returned in `data`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
    delete:
      tags:
        - Pipelines
      summary: Delete a pipeline
      description: |
        Permanently delete a pipeline. Returns `{"data": 1}` on success, `{"data": 0}` if the pipeline was not found.

        > **Warning:** Deleting a pipeline will affect all deals assigned to it.
      operationId: deletePipeline
      parameters:
        - name: pipelineId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Deletion result
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: integer
                    description: "`1` if deleted, `0` if not found"
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/pipelineStages:
    get:
      tags:
        - Pipeline Stages
      summary: List pipeline stages
      description: |
        Returns pipeline stages. Filter by pipeline using `{"pipelineId": "<PIPELINE_UUID>"}` or by workspace using `{"workspaceId": "<WORKSPACE_UUID>"}`.

        Stages are returned in `order` ascending by default, which reflects their position in the pipeline.

        ### Stage types

        | Type | Description |
        |------|-------------|
        | `lead` | Entry stage for new leads |
        | `in-progress` | Active sales stages |
        | `won` | Terminal stage — deal closed successfully |
        | `lost` | Terminal stage — deal lost |
        | `null` | Unclassified stage |

        > **Tip:** Always retrieve stages before creating or updating deals so you can resolve stage names to their UUIDs.
      operationId: listPipelineStages
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PipelineStage'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Pipeline Stages
      summary: Create a pipeline stage
      description: Create a new stage within an existing pipeline.
      operationId: createPipelineStage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PipelineStageCreate'
            example:
              workspaceId: "workspace-uuid"
              pipelineId: "pipeline-uuid"
              name: "Trial"
              type: "in-progress"
              confidence: 0.7
              order: 4
      responses:
        '200':
          description: Pipeline stage created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PipelineStage'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/pipelineStages/{stageId}:
    get:
      tags:
        - Pipeline Stages
      summary: Get a pipeline stage
      description: Retrieve a single pipeline stage by ID.
      operationId: getPipelineStage
      parameters:
        - name: stageId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PipelineStage'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags:
        - Pipeline Stages
      summary: Update a pipeline stage
      description: Update an existing pipeline stage. Returns only the changed fields.
      operationId: updatePipelineStage
      parameters:
        - name: stageId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PipelineStageUpdate'
      responses:
        '200':
          description: Stage updated. Only changed fields are returned in `data`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
    delete:
      tags:
        - Pipeline Stages
      summary: Delete a pipeline stage
      description: |
        Permanently delete a pipeline stage. Returns `{"data": 1}` on success, `{"data": 0}` if not found.

        > **Warning:** Deleting a stage will affect all deals currently assigned to it.
      operationId: deletePipelineStage
      parameters:
        - name: stageId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Deletion result
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: integer
                    description: "`1` if deleted, `0` if not found"
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'


  /api/tasks:
    get:
      tags:
        - Tasks
      summary: List tasks
      description: |
        Returns tasks the user has access to. Filter by workspace using the `where` parameter: `{"workspaceId": "<WORKSPACE_UUID>"}`.

        Common filters:
        - Open tasks: `{"workspaceId": "<WS>", "done": false}`
        - Tasks for a contact: `{"workspaceId": "<WS>", "contactIds": {"$overlaps": ["<CONTACT_UUID>"]}}`
        - Tasks by deadline: `{"workspaceId": "<WS>", "deadline": {"$gte": "now()"}}`
      operationId: listTasks
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Task'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Tasks
      summary: Create a task
      description: |
        Create a new task in a workspace.

        ### content vs description

        Tasks have two Tiptap fields with distinct purposes:

        - **`content`** — The task title as a Tiptap document. If omitted, it is automatically generated from the `name` field.
        - **`description`** — Extended notes or context for the task. Supports rich formatting including headings, bullet lists, and task lists. Optional.

        ### Tiptap document format

        Both `content` and `description` use the same Tiptap document structure:

        ```json
        {
          "type": "doc",
          "content": [
            {
              "type": "paragraph",
              "content": [{"type": "text", "text": "Your text here"}]
            }
          ]
        }
        ```

        Rich formatting example with headings, bold text, bullet lists and task lists:

        ```json
        {
          "type": "doc",
          "content": [
            {
              "type": "heading",
              "attrs": {"level": 3},
              "content": [{"type": "text", "text": "Goal"}]
            },
            {
              "type": "paragraph",
              "content": [
                {"type": "text", "text": "Complete the "},
                {"type": "text", "text": "onboarding proposal", "marks": [{"type": "bold"}]},
                {"type": "text", "text": " before Friday."}
              ]
            },
            {
              "type": "taskList",
              "content": [
                {
                  "type": "taskItem",
                  "attrs": {"checked": false},
                  "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Review requirements"}]}]
                },
                {
                  "type": "taskItem",
                  "attrs": {"checked": false},
                  "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Draft document"}]}]
                }
              ]
            },
            {
              "type": "bulletList",
              "content": [
                {
                  "type": "listItem",
                  "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Keep it concise"}]}]
                }
              ]
            }
          ]
        }
        ```

        ### Priority

        | Value | Label |
        |-------|-------|
        | `null` | No priority |
        | `1` | Low |
        | `2` | Medium |
        | `3` | High |
        | `4` | Urgent |

        ### Linking records

        Tasks can be linked to multiple companies, contacts, and deals simultaneously via `companyIds`, `contactIds`, and `dealIds`.

        > **⚠️ Important:** When updating array fields via `PATCH`, the entire array is **replaced** — not merged. Always include all desired IDs when patching these fields.
      operationId: createTask
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TaskCreate'
            example:
              workspaceId: "workspace-uuid"
              name: "Follow up with contact"
              contactIds: ["contact-uuid"]
              companyIds: ["company-uuid"]
              assignedToIds: ["user-uuid"]
              deadline: "2026-03-10T10:00:00.000Z"
              priority: 3
              description:
                type: "doc"
                content:
                  - type: "paragraph"
                    content:
                      - type: "text"
                        text: "Send a follow-up email after the demo call."
      responses:
        '200':
          description: Task created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Task'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/tasks/{taskId}:
    get:
      tags:
        - Tasks
      summary: Get a task
      description: |
        Retrieve a single task by ID.

        If the task does not exist, the API returns HTTP 200 with an empty body (`{}`).
      operationId: getTask
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Task'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags:
        - Tasks
      summary: Update a task
      description: |
        Update an existing task.

        > **Note:** The response only returns the fields that were changed — not the full task object.

        > **⚠️ Important:** Array fields (`contactIds`, `companyIds`, `dealIds`, `assignedToIds`) are **replaced** on PATCH, not merged. Always include all desired IDs.

        To mark a task as complete: `{"done": true}`
      operationId: updateTask
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TaskUpdate'
            examples:
              markDone:
                summary: Mark as done
                value:
                  done: true
              updateDeadline:
                summary: Update deadline and priority
                value:
                  deadline: "2026-03-15T10:00:00.000Z"
                  priority: 4
              reassign:
                summary: Reassign to different users
                value:
                  assignedToIds: ["user-uuid-1", "user-uuid-2"]
      responses:
        '200':
          description: |
            Task updated successfully. Only the fields that were changed are returned in `data`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    description: Partial task object containing only the updated fields plus `updatedAt` and `updatedById`.
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
    delete:
      tags:
        - Tasks
      summary: Delete a task
      description: Delete a task. Use `archive=true` for soft delete (recoverable), or omit for permanent deletion.
      operationId: deleteTask
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: archive
          in: query
          description: If true, soft deletes (archives) the task instead of permanently deleting it.
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Task deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    description: |
                      Hard delete (`archive` omitted or `false`): returns `1` on success.
                      Soft delete (`archive=true`): returns a single-element array containing the archived task object.
                    oneOf:
                      - type: integer
                        example: 1
                      - type: array
                        items:
                          $ref: '#/components/schemas/Task'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/files:
    get:
      tags:
        - Files
      summary: List files
      description: |
        Returns file records the user has access to. Filter by workspace using the `where` parameter: `{"workspaceId": "<WORKSPACE_UUID>"}`.

        Files can also be filtered by `companyId`, `contactId`, `dealId`, or `noteId` to retrieve files attached to a specific record.

        > **Note:** Files are created by uploading via `POST /api/files/upload` (multipart/form-data), **not** by a JSON `POST /api/files`.
      operationId: listFiles
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/File'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/files/upload:
    post:
      tags:
        - Files
      summary: Upload a file
      description: |
        Upload a file or image using `multipart/form-data`. Unlike the JSON CRUD endpoints, this endpoint does **not** accept a JSON body — send the binary in a `file` form part alongside text form fields.

        The maximum file size is **32 MB**; larger uploads are rejected with HTTP 413.

        ### Upload types

        The `type` form field selects how the file is stored and what is returned:

        - **`file`** — A general document or attachment (PDF, image, video, etc.). The binary is stored in cloud storage and thumbnails/previews are generated for supported content types. Requires `workspaceId`. Can be linked to a record via `companyId`, `contactId`, `dealId`, or `noteId`. Returns the full file record (see the `File` schema).
        - **`image`** — An image asset, typically for `logos`, `avatars`, or inline `attachments`. Requires `folder`. Returns a lightweight object with the hosted image URL.

        ### Thumbnails & previews

        For `type=file`, `thumbnailUrl` and `previewUrl` are generated automatically for these content types: `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `application/pdf`, `video/mp4`, `video/mpeg`, `video/ogg`, `video/webm`, `video/quicktime`. For other types they are `null`.

        ### Downloading

        For `type=file`, the stored `url` points to authenticated storage and is not directly accessible. Use `GET /api/files/download?fileId=<FILE_ID>` to obtain a short-lived signed URL, and `GET /api/files/pages?fileId=<FILE_ID>` to list per-page preview images for multi-page documents.

        ### Example (curl)

        ```bash
        curl -X POST https://api.zero.inc/api/files/upload \
          -H "Authorization: Bearer YOUR_API_TOKEN" \
          -F "type=file" \
          -F "workspaceId=<WORKSPACE_UUID>" \
          -F "companyId=<COMPANY_UUID>" \
          -F "file=@/path/to/proposal.pdf"
        ```
      operationId: uploadFile
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
                - type
              properties:
                file:
                  type: string
                  format: binary
                  description: The binary file contents. Maximum 32 MB.
                type:
                  type: string
                  enum:
                    - file
                    - image
                  description: |
                    `file` stores a general document/attachment; `image` stores an image asset (logos, avatars, inline attachments).
                workspaceId:
                  type: string
                  format: uuid
                  description: Required when `type=file`. Optional when `type=image`.
                folder:
                  type: string
                  enum:
                    - logos
                    - avatars
                    - attachments
                  description: Required when `type=image`. Storage folder for the image.
                transformation:
                  type: string
                  description: |
                    `type=image` only. When set to any non-empty value, the image is scaled down to a maximum of 128×128 px WebP (aspect ratio preserved; used for avatars and logos).
                companyId:
                  type: string
                  format: uuid
                  description: "`type=file` only. Link the file to a company."
                contactId:
                  type: string
                  format: uuid
                  description: "`type=file` only. Link the file to a contact."
                dealId:
                  type: string
                  format: uuid
                  description: "`type=file` only. Link the file to a deal."
                noteId:
                  type: string
                  format: uuid
                  description: "`type=file` only. Link the file to a note."
                placement:
                  type: string
                  description: "`type=file` only. Optional placement/context label for the file."
                description:
                  type: string
                  description: "`type=file` only. Optional human-readable description."
                externalId:
                  type: string
                  description: Optional ID from an external system for integrations.
      responses:
        '200':
          description: |
            File uploaded successfully.

            For `type=file`, `data` is the full file record. For `type=image`, `data` is a lightweight object with the hosted image URL.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    oneOf:
                      - $ref: '#/components/schemas/File'
                      - type: object
                        description: Returned for `type=image` uploads.
                        properties:
                          id:
                            type: string
                            format: uuid
                          assetId:
                            type: string
                            description: Cloudinary asset ID.
                          publidId:
                            type: string
                            description: Cloudinary public ID. (The response field is spelled `publidId`.)
                          url:
                            type: string
                            format: uri
                            description: Hosted image URL.
        '400':
          description: |
            Bad request — for example, `workspaceId` is missing for a `type=file` upload.
          content:
            application/json:
              schema:
                type: object
                properties:
                  statusMessage:
                    type: string
                    example: workspaceId is required for file uploads
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          description: File too large — the file exceeds the 32 MB limit.
          content:
            application/json:
              schema:
                type: object
                properties:
                  statusMessage:
                    type: string
                    example: File size should be less than 32MB

  /api/files/download:
    get:
      tags:
        - Files
      summary: Get a file download URL
      description: |
        Returns a short-lived signed URL for downloading a `type=file` upload from storage. The URL expires **15 minutes** after it is issued.
      operationId: getFileDownloadUrl
      parameters:
        - name: fileId
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Signed download URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri
                    description: Signed, time-limited download URL (valid for 15 minutes).
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          description: |
            Returned when the `fileId` does not exist or is not accessible in the caller's workspace. The handler currently raises an unhandled error, surfaced as HTTP 500 with the message `File not found` (a future release may return 404 instead).
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: File not found

  /api/files/pages:
    get:
      tags:
        - Files
      summary: Get file page previews
      description: |
        Returns per-page thumbnail and preview image URLs for a file, useful for multi-page documents such as PDFs.

        If the file's content type does not support previews, `data` is `false`.
      operationId: getFilePages
      parameters:
        - name: fileId
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: |
            Page previews. `data` is `false` when the file type does not support previews.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    oneOf:
                      - type: boolean
                        example: false
                      - type: object
                        properties:
                          pages:
                            type: array
                            items:
                              type: object
                              properties:
                                thumbnailUrl:
                                  type: string
                                  format: uri
                                previewUrl:
                                  type: string
                                  format: uri
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          description: |
            Returned when the `fileId` does not exist or is not accessible in the caller's workspace. The handler currently raises an unhandled error, surfaced as HTTP 500 with the message `File not found` (a future release may return 404 instead).
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: File not found

  /api/files/{fileId}:
    get:
      tags:
        - Files
      summary: Get a file
      description: |
        Returns a single file record by ID.

        If the file does not exist, the API returns HTTP 200 with an empty body (`{}`).

        To obtain a downloadable URL for the file contents, use `GET /api/files/download?fileId=<FILE_ID>`.
      operationId: getFile
      parameters:
        - name: fileId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/File'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags:
        - Files
      summary: Update file metadata
      description: |
        Update metadata on an existing file (e.g. `name`, `description`, or the record it is linked to). The file's binary contents cannot be changed — re-upload via `POST /api/files/upload` to replace contents.

        > **Note:** The response only returns the fields that were changed — not the full file object.
      operationId: updateFile
      parameters:
        - name: fileId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FileUpdate'
            example:
              description: "Signed proposal, v2"
              dealId: "deal-uuid"
      responses:
        '200':
          description: File updated successfully. Only the fields that were changed are returned in `data`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    description: Partial file object containing only the updated fields plus `updatedAt` and `updatedById`.
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
    delete:
      tags:
        - Files
      summary: Delete a file
      description: Delete a file. Use `archive=true` for soft delete (recoverable), or omit for permanent deletion.
      operationId: deleteFile
      parameters:
        - name: fileId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: archive
          in: query
          description: If true, soft deletes (archives) the file instead of permanently deleting it.
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: File deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    description: |
                      Hard delete (`archive` omitted or `false`): returns `1` on success.
                      Soft delete (`archive=true`): returns a single-element array containing the archived file object.
                    oneOf:
                      - type: integer
                        example: 1
                      - type: array
                        items:
                          $ref: '#/components/schemas/File'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/sequences:
    get:
      tags:
        - Sequences
      summary: List sequences
      description: |
        Returns sequences in a workspace. Filter with the `where` parameter, e.g. `{"workspaceId": "<WORKSPACE_UUID>"}` or `{"workspaceId": "<WORKSPACE_UUID>", "status": "active"}`.

        The `listId` field on each sequence is its **source list** — add that list to a contact's `listIds` to enroll the contact (see the Sequence Enrollments section).
      operationId: listSequences
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Sequence'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/sequences/create:
    post:
      tags:
        - Sequences
      summary: Create a sequence
      description: |
        Creates a draft sequence and provisions everything it needs: a backing **source list** (returned as `listId`), a default `startSequence` step, and a default view.

        Use this endpoint instead of the generic `POST /api/sequences` — the generic verb creates a bare row with no source list, which cannot enroll contacts.

        The new sequence starts in `draft`. Add steps with `POST /api/sequenceSteps`, then call `POST /api/sequences/activate` to start it.
      operationId: createSequence
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - workspaceId
              properties:
                workspaceId:
                  type: string
                  format: uuid
                name:
                  type: string
                  description: Sequence name. Defaults to "New Sequence". Also used as the source list's name.
                sendingWindowTimezone:
                  type: string
                  description: IANA timezone for the start step's sending window (e.g. "America/New_York"). Invalid values fall back to "UTC".
                order:
                  type: integer
                  description: Optional sort order among sequences.
            example:
              workspaceId: "workspace-uuid"
              name: "Q3 Outbound"
      responses:
        '200':
          description: Sequence created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/Sequence'
        '400':
          description: No workspace found
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/sequences/{sequenceId}:
    get:
      tags:
        - Sequences
      summary: Get a sequence
      description: |
        Retrieve a single sequence by ID. If no sequence matches the ID this returns `200` with `data: null` (not a `404`) — the generic CRUD route doesn't 404. The custom action endpoints (`activate`, `pause`, etc.) are the ones that return `404` for a missing sequence.
      operationId: getSequence
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/fields'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Sequence'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags:
        - Sequences
      summary: Update a sequence
      description: |
        Update editable settings on a sequence, such as the default sending `mailboxId`, `linkedinAccountId`, `icon`, `color`, or `settings`.

        > **Do not** change `name` or `status` here. Use `POST /api/sequences/rename` to rename (it keeps the source list name in sync) and `POST /api/sequences/activate` / `POST /api/sequences/pause` to change status (they also transition in-flight contacts).

        If no sequence matches the ID this is a no-op and returns `200` with an empty result (not a `404`).
      operationId: updateSequence
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SequenceUpdate'
            example:
              mailboxId: "mailbox-uuid"
      responses:
        '200':
          description: Sequence updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    description: Partial sequence object containing only the updated fields.
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/sequences/activate:
    post:
      tags:
        - Sequences
      summary: Activate a sequence
      description: |
        Sets a `draft` or `paused` sequence to `active` and begins processing enrolled contacts. Resuming from `paused` re-arms contacts that were paused mid-flight.

        Validation runs first: the sequence must have at least one step after the start step, and every `sendEmail` step must have a sending mailbox (step-level or the sequence's `mailboxId`). If validation fails the endpoint returns `400` with a message.
      operationId: activateSequence
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - sequenceId
              properties:
                sequenceId:
                  type: string
                  format: uuid
            example:
              sequenceId: "sequence-uuid"
      responses:
        '200':
          description: Sequence activated (or already active)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/Sequence'
        '400':
          description: Validation failed (no action step, or an email step has no mailbox)
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/sequences/pause:
    post:
      tags:
        - Sequences
      summary: Pause a sequence
      description: |
        Sets an `active` sequence to `paused` and snapshots every in-flight contact so it can resume from the same point when the sequence is re-activated. A sequence already in `draft` or `paused` is returned unchanged.
      operationId: pauseSequence
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - sequenceId
              properties:
                sequenceId:
                  type: string
                  format: uuid
            example:
              sequenceId: "sequence-uuid"
      responses:
        '200':
          description: Sequence paused (or already paused/draft)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/Sequence'
                  pausedContacts:
                    type: boolean
                    description: Present and `true` when in-flight contacts were paused.
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/sequences/rename:
    post:
      tags:
        - Sequences
      summary: Rename a sequence
      description: Renames the sequence and its source list together, keeping them in sync. Prefer this over patching `name` directly.
      operationId: renameSequence
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - sequenceId
                - name
              properties:
                sequenceId:
                  type: string
                  format: uuid
                name:
                  type: string
            example:
              sequenceId: "sequence-uuid"
              name: "Q3 Outbound (EMEA)"
      responses:
        '200':
          description: Sequence renamed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/sequences/clone:
    post:
      tags:
        - Sequences
      summary: Clone a sequence
      description: |
        Duplicates an existing sequence into a new `draft` sequence with a fresh, empty source list. Steps and sequence-scoped email templates are copied (the auto-managed `aiCompose` step is regenerated; `replyToStepId` references are remapped to the new steps). Enrolled contacts are **not** copied.
      operationId: cloneSequence
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - sourceSequenceId
              properties:
                sourceSequenceId:
                  type: string
                  format: uuid
                name:
                  type: string
                  description: Name for the copy. Defaults to "<source name> (copy)".
            example:
              sourceSequenceId: "sequence-uuid"
      responses:
        '200':
          description: Sequence cloned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/Sequence'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/sequences/delete:
    post:
      tags:
        - Sequences
      summary: Delete a sequence
      description: |
        Permanently deletes a sequence and cascades to its steps, source list and views, email templates, enrollments (`SequenceContactStatus` rows), and any scheduled emails. This is not reversible. Use this instead of the generic `DELETE /api/sequences/{id}`, which would orphan the related records.
      operationId: deleteSequence
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - sequenceId
              properties:
                sequenceId:
                  type: string
                  format: uuid
            example:
              sequenceId: "sequence-uuid"
      responses:
        '200':
          description: Sequence deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/sequences/reenroll:
    post:
      tags:
        - Sequence Enrollments
      summary: Re-enroll contacts
      description: |
        Resets the given contacts to the start of the sequence (status `waitingForPickUp`), clearing their step history, outcome, scheduled emails, enrollment logs, and any AI drafts. Contacts with no existing enrollment row are enrolled fresh. The background worker re-assigns the first step on the next pass (if the sequence is active).
      operationId: reenrollSequenceContacts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - sequenceId
                - contactIds
              properties:
                sequenceId:
                  type: string
                  format: uuid
                contactIds:
                  type: array
                  items:
                    type: string
                    format: uuid
            example:
              sequenceId: "sequence-uuid"
              contactIds: ["contact-uuid-1", "contact-uuid-2"]
      responses:
        '200':
          description: Contacts re-enrolled
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  count:
                    type: integer
                    description: Number of contacts processed.
        '400':
          description: sequenceId or contactIds missing, or the sequence has no source list
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/sequences/forceNextStep:
    post:
      tags:
        - Sequence Enrollments
      summary: Advance contacts to the next step
      description: |
        Moves the given contacts from their current step to the next one immediately, cancelling any pending scheduled emails for the current step. Contacts already on the last step are marked `completed`; contacts with no current step are skipped. When the next step is a `delay`, the contact parks on the delay and resumes naturally when it elapses.
      operationId: forceNextSequenceStep
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - sequenceId
                - contactIds
              properties:
                sequenceId:
                  type: string
                  format: uuid
                contactIds:
                  type: array
                  items:
                    type: string
                    format: uuid
            example:
              sequenceId: "sequence-uuid"
              contactIds: ["contact-uuid-1"]
      responses:
        '200':
          description: Contacts advanced
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  advanced:
                    type: integer
                  completed:
                    type: integer
                  skipped:
                    type: integer
        '400':
          description: sequenceId or contactIds missing
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/sequences/progressSummary:
    get:
      tags:
        - Sequence Enrollments
      summary: Sequence progress metrics
      description: |
        Returns aggregate enrollment metrics per sequence for a workspace (enrolled, active, paused, completed, replied, bounced, etc.). Pass `sequenceId` to scope the summary to a single sequence.
      operationId: getSequenceProgressSummary
      parameters:
        - name: workspaceId
          in: query
          required: true
          schema:
            type: string
            format: uuid
        - name: sequenceId
          in: query
          required: false
          description: Limit the summary to a single sequence.
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SequenceProgressSummaryEntry'
        '400':
          description: workspaceId missing
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/sequenceContactStatuses:
    get:
      tags:
        - Sequence Enrollments
      summary: List enrollments
      description: |
        Returns enrollment records (`SequenceContactStatus`) — one per contact per sequence — with each contact's live status, current step, and outcome. Filter with `where`, e.g. `{"workspaceId": "<WORKSPACE_UUID>", "sequenceId": "<SEQUENCE_UUID>"}`, optionally adding `"status": "active"`.
      operationId: listSequenceContactStatuses
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SequenceContactStatus'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/sequenceSteps:
    get:
      tags:
        - Sequences
      summary: List sequence steps
      description: |
        Returns the steps of a sequence. Filter with `where`, e.g. `{"workspaceId": "<WORKSPACE_UUID>", "sequenceId": "<SEQUENCE_UUID>"}`, and order by `order`: `orderBy={"order":"asc"}`.
      operationId: listSequenceSteps
      parameters:
        - $ref: '#/components/parameters/fields'
        - $ref: '#/components/parameters/where'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/orderBy'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SequenceStep'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags:
        - Sequences
      summary: Add a step
      description: |
        Adds a step to a sequence. Set `type` to one of `sendEmail`, `delay`, `addTask`, `sendLinkedinMessage`, `sendLinkedinConnectionRequest`, `startAutomation`, or `endSequence`, and `order` to position it after the start step (which is `order` 0).

        Per-step configuration lives in the freeform `settings` and `triggerSettings` objects, and **the fields differ by `type`**. The reference below lists the commonly-used fields for each type.

        **Applies to every type:**

        - `settings.backgroundPhases` is managed by the server — don't set it; whatever you send is overwritten.
        - Text fields on email, LinkedIn, and task steps support macros such as `{{contact.firstName}}`, `{{contact.email}}`, `{{contact.company.name}}`, and `{{mailbox.name}}`.
        - You don't create the `startSequence` step (order 0) — it ships with the sequence, but you can `PATCH` it to tune scheduling (see below). Never create an `aiCompose` step; it is auto-managed — enable AI instead by setting `settings.ai.enabled = true` on a send step.

        ### `sendEmail`

        | Field | Type | Notes |
        | --- | --- | --- |
        | `settings.mailboxId` | uuid | Sending mailbox. Falls back to the sequence's `mailboxId`; one of the two is required to activate. |
        | `settings.subject` | string | Subject line (macros supported). |
        | `settings.content` | Tiptap doc | Email body (macros supported). |
        | `settings.draft` | boolean | `true` creates a draft for a user to send manually; `false` (or omitted) sends automatically. |
        | `settings.replyToStepId` | uuid | Send as a reply within an earlier `sendEmail` step's thread. Must reference an earlier email step in the same sequence. |
        | `settings.stopOnReply` | boolean | End the sequence for a contact who replies. |
        | `settings.ai` | object | Set `{"enabled": true, "mode": "full"}` to AI-compose the email (this adds the auto-managed `aiCompose` step). |

        The recipient is always the contact's email, and `from` defaults to the mailbox identity.

        ```json
        {
          "settings": {
            "mailboxId": "<MAILBOX_UUID>",
            "subject": "Quick question, {{contact.firstName}}",
            "content": {"type": "doc", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Hi {{contact.firstName}}, wanted to reach out."}]}]},
            "draft": false
          }
        }
        ```

        ### `delay`

        Waits before the next step runs. Configured via `triggerSettings`, not `settings`.

        | Field | Type | Notes |
        | --- | --- | --- |
        | `triggerSettings.delayMs` | number | Wait, in milliseconds. Defaults to `86400000` (24h) if omitted. |

        ```json
        { "triggerSettings": { "delayMs": 172800000 } }
        ```

        ### `addTask`

        | Field | Type | Notes |
        | --- | --- | --- |
        | `settings.title` | string | Task title (macros supported). |
        | `settings.description` | Tiptap doc / string | Task body. |
        | `settings.taskType` | string | e.g. `call`, `todo`, `meeting`, `follow_up`. |
        | `settings.priority` | integer | `1` Low to `4` Urgent. |
        | `settings.assignedToIds` | uuid[] | Assignee user ids. Defaults to the sequence creator. |
        | `settings.continueMode` | string | When the sequence advances: `immediate`, `done` (default), or `deadline`. |
        | `settings.deadlineDays` | number | Days after creation for the deadline. |
        | `settings.deadlineTime` | string | `HH:MM` for the deadline (default `09:00`). |
        | `settings.deadlineTimezone` | string | IANA timezone for the deadline. |

        ```json
        {
          "settings": {
            "title": "Call {{contact.firstName}}",
            "taskType": "call",
            "priority": 3,
            "continueMode": "done",
            "deadlineDays": 2
          }
        }
        ```

        ### `sendLinkedinMessage` and `sendLinkedinConnectionRequest`

        | Field | Type | Notes |
        | --- | --- | --- |
        | `settings.userId` | uuid | **Required.** The workspace user whose connected LinkedIn account sends. |
        | `settings.content` | Tiptap doc | Message body / connection note (macros supported). Connection notes are truncated to 300 characters. |
        | `settings.skipIfNoLinkedinData` | boolean | Both types — skip the contact (instead of erroring) when they don't have a complete LinkedIn profile. |
        | `settings.skipIfNotConnected` | boolean | Messages only — skip contacts who aren't 1st-degree connections instead of erroring. |
        | `settings.stopIfNotAccepted` | boolean | Connection requests only — end the sequence if the request isn't accepted within `settings.stopIfNotAcceptedDays` days. Requires `stopIfNotAcceptedDays` to be set. |
        | `settings.stopIfNotAcceptedDays` | number | Connection requests only — days to wait before ending the sequence when `stopIfNotAccepted` is on. `0` or omitted means no timeout is scheduled (the sequence won't end on non-acceptance). |

        ```json
        {
          "settings": {
            "userId": "<WORKSPACE_USER_UUID>",
            "content": {"type": "doc", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Hi {{contact.firstName}}!"}]}]}
          }
        }
        ```

        ### `startAutomation`

        | Field | Type | Notes |
        | --- | --- | --- |
        | `settings.automationId` | uuid | **Required.** The manual-trigger automation to run for the contact. |

        ### `endSequence`

        Takes no settings — it marks the contact complete.

        ### `startSequence` (PATCH only)

        Created with the sequence; `PATCH /api/sequenceSteps/{stepId}` to adjust enrollment and scheduling.

        | Field | Type | Notes |
        | --- | --- | --- |
        | `settings.rollMode` | string | `auto` (default) enrolls contacts automatically; `manual` holds new enrollments in `waitingForApproval` until approved in the app. |
        | `settings.dailyStarts` | number | Max contacts that may start the sequence per day. |
        | `settings.activeDays` | string[] | Days sends are allowed, e.g. `["mon", "tue", "wed", "thu", "fri"]`. |
        | `settings.sendingWindow` | object | `{"start": "09:00", "end": "17:00", "timezone": "<IANA>"}`. |
        | `settings.staggerMinutes` | number | Minimum minutes between auto-sent emails. |
      operationId: createSequenceStep
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SequenceStepCreate'
            example:
              workspaceId: "workspace-uuid"
              sequenceId: "sequence-uuid"
              type: "delay"
              name: "Wait 2 days"
              order: 1
              triggerSettings:
                delayMs: 172800000
      responses:
        '200':
          description: Step created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/SequenceStep'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags:
        - Sequences
      summary: Batch update steps
      description: |
        Updates multiple steps in one request — typically to reorder them. The body is an object keyed by step ID, where each value is the partial update for that step.
      operationId: batchUpdateSequenceSteps
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties:
                $ref: '#/components/schemas/SequenceStepUpdate'
            example:
              "step-uuid-1":
                order: 1
              "step-uuid-2":
                order: 2
      responses:
        '200':
          description: Steps updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SequenceStep'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/sequenceSteps/{stepId}:
    patch:
      tags:
        - Sequences
      summary: Update a step
      description: Update a single sequence step.
      operationId: updateSequenceStep
      parameters:
        - name: stepId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SequenceStepUpdate'
            example:
              name: "Intro email"
              subject: "Quick question"
      responses:
        '200':
          description: Step updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    description: Partial step object containing only the updated fields.
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'
    delete:
      tags:
        - Sequences
      summary: Delete a step
      description: Delete a sequence step. Use `archive=true` for a recoverable soft delete. The `startSequence` step should not be deleted.
      operationId: deleteSequenceStep
      parameters:
        - name: stepId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: archive
          in: query
          description: If true, soft deletes (archives) the step instead of permanently deleting it.
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Step deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    description: |
                      Hard delete (`archive` omitted or `false`): returns `1` on success.
                      Soft delete (`archive=true`): returns a single-element array containing the archived step object.
                    oneOf:
                      - type: integer
                        example: 1
                      - type: array
                        items:
                          $ref: '#/components/schemas/SequenceStep'
                  sideEffects:
                    type: array
                    items: {}
        '401':
          $ref: '#/components/responses/Unauthorized'


components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: All API requests require a Bearer token in the Authorization header. Create an API key from [Workspace Settings → API keys](https://app.zero.inc/settings/workspace/api)
      x-gitbook-token-placeholder: YOUR_API_TOKEN

  parameters:
    fields:
      name: fields
      in: query
      description: Comma-separated list of fields to return. Defaults to all fields.
      schema:
        type: string
      example: id,name,domain
    where:
      name: where
      in: query
      description: |
        JSON-encoded filter object. All top-level conditions are combined with AND logic. Use `$or` for OR logic.
        
        The available operators depend on the **data type** of the field you are filtering on.

        ---

        ## String fields
        Fields like `name`, `domain`, `description`, `linkedin`, `source`, `externalId`, `location`.

        | Operator | Description | Example |
        |----------|-------------|---------|
        | *(exact)* | Exact match | `{"name": "Linear"}` |
        | `$eq` | Explicit exact match | `{"name": {"$eq": "Linear"}}` |
        | `$not` | Not equal | `{"source": {"$not": "import"}}` |
        | `$in` | Matches any value in array | `{"domain": {"$in": ["linear.app", "granola.so"]}}` |
        | `$notIn` | Matches none of the values | `{"source": {"$notIn": ["import", "api"]}}` |
        | `$contains` | Case-insensitive word-boundary substring match | `{"name": {"$contains": "YC"}}` |
        | `$notContains` | Does not contain | `{"name": {"$notContains": "Test"}}` |
        | `$containsAny` | Contains any of the given strings | `{"name": {"$containsAny": ["YC", "Techstars"]}}` |
        | `$startsWith` | Starts with prefix | `{"domain": {"$startsWith": "app."}}` |
        | `$endsWith` | Ends with suffix | `{"email": {"$endsWith": "@zero.inc"}}` |
        | `$exists` | Field is present and truthy | `{"linkedin": {"$exists": true}}` |
        | `$notExists` | Field is absent, null, or empty | `{"linkedin": {"$notExists": true}}` |

        ---

        ## Number fields
        Fields like `value`, `confidence`.

        | Operator | Description | Example |
        |----------|-------------|---------|
        | *(exact)* | Exact match | `{"value": 5000}` |
        | `$eq` | Explicit exact match | `{"value": {"$eq": 5000}}` |
        | `$not` | Not equal | `{"value": {"$not": 0}}` |
        | `$gt` | Greater than | `{"value": {"$gt": 10000}}` |
        | `$gte` | Greater than or equal | `{"value": {"$gte": 5000}}` |
        | `$lt` | Less than | `{"value": {"$lt": 10000}}` |
        | `$lte` | Less than or equal | `{"value": {"$lte": 50000}}` |
        | `$in` | Matches any value in array | `{"confidence": {"$in": [0.25, 0.5, 0.75]}}` |
        | `$notIn` | Matches none of the values | `{"confidence": {"$notIn": [0, 1]}}` |
        | `$exists` | Field is present and truthy | `{"value": {"$exists": true}}` |
        | `$notExists` | Field is absent, null, or zero | `{"value": {"$notExists": true}}` |

        Multiple operators can be combined on one field:
        ```json
        {"value": {"$gte": 5000, "$lt": 10000}}
        ```

        ---

        ## Date fields
        Fields like `closeDate`, `startDate`, `endDate`, `createdAt`, `updatedAt`.

        Values can be ISO 8601 strings (`"2026-01-01"`, `"2026-01-01T00:00:00Z"`) or **relative time macros**.

        **Relative time macros:** `+Nd` / `-Nd` (days), `+Nw` / `-Nw` (weeks), `+Nm` / `-Nm` (months), `+Ny` / `-Ny` (years), `+Nh` / `-Nh` (hours), `+Ns` / `-Ns` (seconds), `now()`.

        | Operator | Description | Example |
        |----------|-------------|---------|
        | `$gte` | On or after | `{"closeDate": {"$gte": "2026-01-01"}}` |
        | `$lte` | On or before | `{"closeDate": {"$lte": "2026-03-31"}}` |
        | `$gt` | After | `{"createdAt": {"$gt": "2026-01-01T00:00:00Z"}}` |
        | `$lt` | Before | `{"createdAt": {"$lt": "now()"}}` |
        | `$date` | Exact date match (compares date portion only) | `{"closeDate": {"$date": "2026-01-15"}}` |
        | `$exists` | Field is present and truthy | `{"closeDate": {"$exists": true}}` |
        | `$notExists` | Field is absent or null | `{"closeDate": {"$notExists": true}}` |

        Date range example:
        ```json
        {"closeDate": {"$gte": "2026-01-01", "$lte": "2026-03-31"}}
        ```
        Relative date example (closing in next 30 days):
        ```json
        {"closeDate": {"$gte": "now()", "$lte": "+30d"}}
        ```

        ---

        ## Array fields
        Fields like `listIds`, `ownerIds`, `contactIds`.

        | Operator | Description | Example |
        |----------|-------------|---------|
        | `$includes` | Array contains the given value (use this — bare exact match is not supported) | `{"listIds": {"$includes": "<LIST_UUID>"}}` |
        | `$notIncludes` | Array does not contain the given value | `{"ownerIds": {"$notIncludes": "<USER_UUID>"}}` |
        | `$overlaps` | Array contains at least one of the given values | `{"ownerIds": {"$overlaps": ["<UUID_1>", "<UUID_2>"]}}` |
        | `$notOverlaps` | Array contains none of the given values | `{"listIds": {"$notOverlaps": ["<UUID_1>", "<UUID_2>"]}}` |
        | `$all` | Array contains all of the given values | `{"listIds": {"$all": ["<UUID_1>", "<UUID_2>"]}}` |
        | `$length` | Filter by array length (supports nested operators) | `{"contactIds": {"$length": {"$gte": 2}}}` |
        | `$exists` | Field is present and non-empty | `{"ownerIds": {"$exists": true}}` |
        | `$notExists` | Field is absent or empty | `{"ownerIds": {"$notExists": true}}` |

        ---

        ## UUID / ID fields
        Fields like `id`, `workspaceId`, `companyId`, `stage`.

        | Operator | Description | Example |
        |----------|-------------|---------|
        | *(exact)* | Exact match | `{"stage": "<PIPELINE_STAGE_UUID>"}` |
        | `$in` | Matches any value in array | `{"stage": {"$in": ["<UUID_1>", "<UUID_2>"]}}` |
        | `$notIn` | Matches none of the values | `{"stage": {"$notIn": ["<UUID_1>"]}}` |
        | `$not` | Not equal | `{"stage": {"$not": "<UUID>"}}` |

        ---

        ## Boolean fields
        Fields like `archived`.

        | Operator | Description | Example |
        |----------|-------------|---------|
        | *(exact)* | Exact match | `{"archived": false}` |
        | `$not` | Not equal | `{"archived": {"$not": true}}` |

        ---

        ## Logical operators
        These work across all data types.

        `$or` — matches if **any** sub-condition is true:
        ```json
        {
          "workspaceId": "<WORKSPACE_UUID>",
          "$or": [
            {"ownerIds": {"$overlaps": ["<USER_UUID_1>", "<USER_UUID_2>"]}},
            {"closeDate": {"$gte": "2026-01-01"}}
          ]
        }
        ```

        `$and` — matches if **all** sub-conditions are true (useful when you need multiple conditions on the same field):
        ```json
        {
          "$and": [
            {"name": {"$contains": "Enterprise"}},
            {"name": {"$notContains": "Test"}}
          ]
        }
        ```

        ---

        ## Dot-notation (relation filtering)
        Use dot-syntax to filter records based on properties of related objects:
        ```json
        {"company.name": "Linear"}
        {"company.domain": {"$in": ["linear.app", "granola.so"]}}
        {"company.location.city": "San Francisco"}
        {"companyProfile.categories": {"$overlaps": ["Sales", "Marketing"]}}
        ```
        All operators available for the target field's data type can be used with dot-notation.

        ---

        ## Custom property filtering
        Custom properties are stored in the `custom` object on records, keyed by UUID. Use `GET /api/columns` to discover the available custom property IDs, types, and options for a workspace.
        
        Filter on custom properties using `custom.<COLUMN_ID>` with operators appropriate for the column's type.
        
        > **Note:** For `select` and `multiselect` columns, option values are UUIDs (the `key` field from the column's `options` array). Use the UUID, not the human-readable name.
        
        ```json
        // Text custom property
        {"custom.54e1ca7d-69c3-4b77-8266-8085b5834116": {"$contains": "enterprise"}}
        
        // Select custom property (use the option key UUID)
        {"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890": "3e839b5c-b311-4887-b2da-727d2d75cdd6"}
        
        // Multi-select custom property (use option key UUIDs)
        {"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901": {"$overlaps": ["a1b1c1d1-e1f1-1111-aaaa-111111111111", "b2b2c2d2-e2f2-2222-bbbb-222222222222"]}}
        
        // Currency/number custom property
        {"custom.c3d4e5f6-a7b8-9012-cdef-123456789012": {"$gte": 100000}}
        
        // Date custom property
        {"custom.d4e5f6a7-b8c9-0123-defa-234567890123": {"$gte": "2026-01-01"}}
        
        // Boolean custom property
        {"custom.e5f6a7b8-c9d0-1234-efab-345678901234": true}
        
        // Check if custom property has a value
        {"custom.f6a7b8c9-d0e1-2345-fabc-456789012345": {"$exists": true}}
        ```

        ---

        ## Complex example
        All top-level keys are ANDed together:
        ```json
        {
          "name": {"$contains": "Zero"},
          "location.city": "Helsinki",
          "location.country": {"$in": ["United Kingdom", "Germany", "Sweden"]},
          "value": {"$gte": 10000},
          "closeDate": {"$gte": "2026-01-01", "$lte": "2026-03-31"},
          "ownerIds": {"$includes": "<USER_UUID>"},
          "stage": {"$in": ["<UUID_1>", "<UUID_2>"]},
          "lastActivity": {"$exists": true},
          "companyProfile.categories": {"$overlaps": ["Sales", "Marketing"]},
          "custom.54e1ca7d-69c3-4b77-8266-8085b5834116": {"$contains": "enterprise"}
        }
        ```
      schema:
        type: string
      example: '{"stage":"<PIPELINE_STAGE_UUID>"}'
    limit:
      name: limit
      in: query
      description: Maximum number of records to return
      schema:
        type: integer
        default: 100
    offset:
      name: offset
      in: query
      description: Pagination offset
      schema:
        type: integer
        default: 0
    orderBy:
      name: orderBy
      in: query
      description: JSON string for sort order
      schema:
        type: string
      example: '{"name":"asc"}'

  responses:
    Unauthorized:
      description: Authentication failed or token is invalid/expired
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: Session not found
    NotFound:
      description: The requested resource was not found
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: Not found

  schemas:
    Company:
      type: object
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
        domain:
          type: string
        logo:
          type: string
          format: uri
        description:
          type: string
        linkedin:
          type: string
        listIds:
          type: array
          items:
            type: string
            format: uuid
        ownerIds:
          type: array
          items:
            type: string
            format: uuid
        location:
          type: object
          description: |
            Geographic location of the company. Stored as a structured object, not a plain string.
          properties:
            city:
              type: string
            state:
              type: string
            country:
              type: string
            continent:
              type: string
            countryCode:
              type: string
            stateCode:
              type: string
            coordinates:
              type: object
              properties:
                lat:
                  type: number
                lng:
                  type: number
        parentCompanyId:
          type: string
          format: uuid
          nullable: true
          description: ID of the parent company, for hierarchical company structures.
        custom:
          type: object
          description: |
            Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `"custom.<COLUMN_ID>": "value"`) to avoid overwriting other custom properties.
        externalId:
          type: string
          description: ID from external system for integrations
        source:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        archived:
          type: boolean
        createdById:
          type: string
          format: uuid

    CompanyCreate:
      type: object
      required:
        - workspaceId
      properties:
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
        domain:
          type: string
        logo:
          type: string
          format: uri
        description:
          type: string
        linkedin:
          type: string
        listIds:
          type: array
          items:
            type: string
            format: uuid
        ownerIds:
          type: array
          items:
            type: string
            format: uuid
        location:
          type: object
          description: Geographic location as a structured object. Do not pass a plain string.
          properties:
            city:
              type: string
            state:
              type: string
            country:
              type: string
            continent:
              type: string
        parentCompanyId:
          type: string
          format: uuid
          description: ID of the parent company.
        custom:
          type: object
        externalId:
          type: string
        source:
          type: string

    CompanyUpdate:
      type: object
      properties:
        name:
          type: string
        domain:
          type: string
        logo:
          type: string
          format: uri
        description:
          type: string
        linkedin:
          type: string
        listIds:
          type: array
          items:
            type: string
            format: uuid
        ownerIds:
          type: array
          items:
            type: string
            format: uuid
        location:
          type: object
          description: Geographic location as a structured object. Do not pass a plain string.
          properties:
            city:
              type: string
            state:
              type: string
            country:
              type: string
            continent:
              type: string
        parentCompanyId:
          type: string
          format: uuid
        custom:
          type: object
        externalId:
          type: string
        source:
          type: string

    Contact:
      type: object
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        companyId:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: string
          format: email
        title:
          type: string
        phone:
          type: string
        linkedin:
          type: string
        x:
          type: string
          description: X (formerly Twitter) handle
        facebook:
          type: string
        github:
          type: string
        avatar:
          type: string
          format: uri
        location:
          type: object
          description: |
            Geographic location of the contact. Stored as a structured object, not a plain string.
          properties:
            city:
              type: string
            state:
              type: string
            country:
              type: string
            continent:
              type: string
            countryCode:
              type: string
            stateCode:
              type: string
            coordinates:
              type: object
              properties:
                lat:
                  type: number
                lng:
                  type: number
        type:
          type: string
        custom:
          type: object
          description: |
            Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `"custom.<COLUMN_ID>": "value"`) to avoid overwriting other custom properties.
        listIds:
          type: array
          items:
            type: string
            format: uuid
        ownerIds:
          type: array
          items:
            type: string
            format: uuid
        externalId:
          type: string
        source:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        archived:
          type: boolean
        createdById:
          type: string
          format: uuid

    ContactCreate:
      type: object
      required:
        - workspaceId
      properties:
        workspaceId:
          type: string
          format: uuid
        companyId:
          type: string
          format: uuid
          description: If not provided but email or linkedin is given, the system will automatically find or create the associated company.
        name:
          type: string
        email:
          type: string
          format: email
        title:
          type: string
        phone:
          type: string
        linkedin:
          type: string
        x:
          type: string
          description: X (formerly Twitter) handle
        facebook:
          type: string
        github:
          type: string
        avatar:
          type: string
          format: uri
        location:
          type: object
          description: Geographic location as a structured object. Do not pass a plain string.
          properties:
            city:
              type: string
            state:
              type: string
            country:
              type: string
            continent:
              type: string
        type:
          type: string
        custom:
          type: object
        listIds:
          type: array
          items:
            type: string
            format: uuid
        ownerIds:
          type: array
          items:
            type: string
            format: uuid
        externalId:
          type: string
        source:
          type: string

    ContactUpdate:
      type: object
      properties:
        companyId:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: string
          format: email
        title:
          type: string
        phone:
          type: string
        linkedin:
          type: string
        x:
          type: string
          description: X (formerly Twitter) handle
        facebook:
          type: string
        github:
          type: string
        avatar:
          type: string
          format: uri
        location:
          type: object
          description: Geographic location as a structured object. Do not pass a plain string.
          properties:
            city:
              type: string
            state:
              type: string
            country:
              type: string
            continent:
              type: string
        type:
          type: string
        custom:
          type: object
        listIds:
          type: array
          items:
            type: string
            format: uuid
        ownerIds:
          type: array
          items:
            type: string
            format: uuid
        externalId:
          type: string
        source:
          type: string

    Deal:
      type: object
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        pipelineId:
          type: string
          format: uuid
          description: The pipeline this deal belongs to.
        companyId:
          type: string
          format: uuid
        contactIds:
          type: array
          items:
            type: string
            format: uuid
        name:
          type: string
        stage:
          type: string
          description: UUID of the pipeline stage.
        value:
          type: number
        confidence:
          type: number
          minimum: 0
          maximum: 1
          description: |
            Win probability as a decimal fraction between 0 and 1 (e.g. `0.75` = 75%).
            Do not use percentages (0–100) — values outside the 0–1 range will fail.
        closeDate:
          type: string
          format: date
        startDate:
          type: string
          format: date
        endDate:
          type: string
          format: date
        listIds:
          type: array
          items:
            type: string
            format: uuid
        ownerIds:
          type: array
          items:
            type: string
            format: uuid
        custom:
          type: object
          description: |
            Custom property values, keyed by column UUID. Use `GET /api/columns` to discover available custom property IDs and types. When updating via PATCH, always use dot-notation (e.g. `"custom.<COLUMN_ID>": "value"`) to avoid overwriting other custom properties.
        externalId:
          type: string
        source:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        archived:
          type: boolean
        createdById:
          type: string
          format: uuid

    DealCreate:
      type: object
      required:
        - workspaceId
      properties:
        workspaceId:
          type: string
          format: uuid
        companyId:
          type: string
          format: uuid
        contactIds:
          type: array
          items:
            type: string
            format: uuid
        name:
          type: string
        stage:
          type: string
          description: If not provided, defaults to the workspace defaultDealStage setting.
        value:
          type: number
        confidence:
          type: number
          minimum: 0
          maximum: 1
          description: Win probability as a decimal fraction between 0 and 1 (e.g. `0.75` = 75%). Do not use percentages.
        closeDate:
          type: string
          format: date
        startDate:
          type: string
          format: date
        endDate:
          type: string
          format: date
        listIds:
          type: array
          items:
            type: string
            format: uuid
        ownerIds:
          type: array
          items:
            type: string
            format: uuid
        custom:
          type: object
        externalId:
          type: string
        source:
          type: string

    DealUpdate:
      type: object
      properties:
        companyId:
          type: string
          format: uuid
        contactIds:
          type: array
          items:
            type: string
            format: uuid
        name:
          type: string
        stage:
          type: string
        value:
          type: number
        confidence:
          type: number
          minimum: 0
          maximum: 1
          description: Win probability as a decimal fraction between 0 and 1 (e.g. `0.75` = 75%). Do not use percentages.
        closeDate:
          type: string
          format: date
        startDate:
          type: string
          format: date
        endDate:
          type: string
          format: date
        listIds:
          type: array
          items:
            type: string
            format: uuid
        ownerIds:
          type: array
          items:
            type: string
            format: uuid
        custom:
          type: object
        externalId:
          type: string
        source:
          type: string

    Column:
      type: object
      description: A custom property definition. The `id` is used as the key inside the `custom` object on records.
      properties:
        id:
          type: string
          format: uuid
          description: The unique identifier for this custom property. Use this as the key when filtering or updating custom fields on records.
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
          description: Human-readable name of the custom property
        description:
          type: string
          nullable: true
          description: Optional description of the custom property
        key:
          type: string
          description: Dot-notation path for this column, e.g. `custom.<UUID>`. Convenient for constructing filter and update payloads.
        type:
          type: string
          enum:
            - text
            - richtext
            - number
            - currency
            - date
            - timestamp
            - select
            - multiselect
            - boolean
            - url
            - email
            - phone
            - user
            - company
            - companies
            - contact
            - contacts
          description: |
            The data type of the custom property.
            
            | Type | Description |
            |------|-------------|
            | `text` | Plain text string |
            | `richtext` | Rich-text / formatted content |
            | `number` | Numeric value |
            | `currency` | Monetary value |
            | `date` | Date (no time) |
            | `timestamp` | Date and time |
            | `select` | Single choice from a predefined list — use option `key` UUIDs |
            | `multiselect` | Multiple choices from a predefined list — use option `key` UUIDs |
            | `boolean` | True/false |
            | `url` | URL string |
            | `email` | Email address |
            | `phone` | Phone number |
            | `user` | Reference to a workspace user |
            | `company` | Relation to a single company record |
            | `companies` | Relation to multiple company records |
            | `contact` | Relation to a single contact record |
            | `contacts` | Relation to multiple contact records |
        entity:
          type: string
          enum:
            - companies
            - contacts
            - deals
          description: Which record type this custom property belongs to (plural form).
        options:
          type: array
          nullable: true
          description: |
            Available options for `select` and `multiselect` type columns. `null` for all other types.
            
            > **Important:** Use the option `key` UUID (not the `name`) when filtering or updating records.
          items:
            type: object
            properties:
              key:
                type: string
                format: uuid
                description: The UUID value stored on records. Use this when filtering (`$in`, `$overlaps`, exact match) or writing to a select/multiselect field.
              name:
                type: string
                description: Human-readable display name for the option
              color:
                type: string
                description: Hex color code for UI display (e.g. `#4CAF50`)
        ai:
          type: boolean
          description: Whether this column is AI-generated
        archived:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        createdById:
          type: string
          format: uuid

    Note:
      type: object
      description: A note attached to one or more records (company, contact, or deal).
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
          description: Title of the note
        emoji:
          type: string
          description: Emoji icon for the note (e.g. "🗒️", "🔥")
        content:
          type: object
          nullable: true
          description: |
            Note body in [Tiptap](https://tiptap.dev/) document format. A JSON object with `type: "doc"` at the root containing an array of block nodes.

            ```json
            {
              "type": "doc",
              "content": [
                {
                  "type": "paragraph",
                  "content": [
                    {"type": "text", "text": "Your note text here"}
                  ]
                }
              ]
            }
            ```
        companyId:
          type: string
          format: uuid
          nullable: true
        contactId:
          type: string
          format: uuid
          nullable: true
        dealId:
          type: string
          format: uuid
          nullable: true
        archived:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        createdById:
          type: string
          format: uuid
        externalId:
          type: string
          nullable: true
        source:
          type: string
          nullable: true

    NoteCreate:
      type: object
      required:
        - workspaceId
      properties:
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
          description: Title of the note
        emoji:
          type: string
          description: Emoji icon for the note
        content:
          type: object
          description: Note body in Tiptap document format. Can be omitted and set later via PATCH.
        companyId:
          type: string
          format: uuid
          description: Associate the note with a company
        contactId:
          type: string
          format: uuid
          description: Associate the note with a contact
        dealId:
          type: string
          format: uuid
          description: Associate the note with a deal
        externalId:
          type: string
        source:
          type: string

    NoteUpdate:
      type: object
      properties:
        name:
          type: string
        emoji:
          type: string
        content:
          type: object
          description: Replaces the entire note body. Must be a valid Tiptap document.
        companyId:
          type: string
          format: uuid
        contactId:
          type: string
          format: uuid
        dealId:
          type: string
          format: uuid
        externalId:
          type: string
        source:
          type: string

    Task:
      type: object
      description: A task that can be assigned to users and linked to companies, contacts, and deals.
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
          description: Title of the task
        done:
          type: boolean
          description: Whether the task has been completed
          default: false
        priority:
          type: integer
          nullable: true
          description: |
            Task priority level.
            - `null` — No priority
            - `1` — Low
            - `2` — Medium
            - `3` — High
            - `4` — Urgent
          enum: [null, 1, 2, 3, 4]
        deadline:
          type: string
          format: date-time
          nullable: true
          description: Due date and time (ISO 8601)
        content:
          type: object
          description: Task title as a Tiptap document. Auto-generated from `name` if not provided on creation.
        description:
          type: object
          description: Extended notes or context for the task, in Tiptap document format. Supports rich formatting.
        companyIds:
          type: array
          items:
            type: string
            format: uuid
          description: Companies linked to this task
        contactIds:
          type: array
          items:
            type: string
            format: uuid
          description: Contacts linked to this task
        dealIds:
          type: array
          items:
            type: string
            format: uuid
          description: Deals linked to this task
        assignedToIds:
          type: array
          items:
            type: string
            format: uuid
          description: User IDs this task is assigned to
        type:
          type: string
          nullable: true
          description: |
            Set automatically by integrations (e.g. `circleback` for meeting-generated tasks, `ai-generated` for AI-created tasks). Not intended to be set manually via the API.
        message:
          type: string
          nullable: true
        archived:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        createdById:
          type: string
          format: uuid
          nullable: true
        externalId:
          type: string
          nullable: true
        source:
          type: string
          nullable: true

    TaskCreate:
      type: object
      required:
        - workspaceId
        - name
      properties:
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
          description: Title of the task. Also used to auto-generate `content` if `content` is not provided.
        content:
          type: object
          description: Task title as a Tiptap document. If omitted, auto-generated from `name`.
        description:
          type: object
          description: Extended notes in Tiptap document format. Supports headings, bullet lists, task lists, and inline formatting.
        done:
          type: boolean
          default: false
        priority:
          type: integer
          nullable: true
          description: "`null` = No priority, `1` = Low, `2` = Medium, `3` = High, `4` = Urgent"
          enum: [null, 1, 2, 3, 4]
        deadline:
          type: string
          format: date-time
        companyIds:
          type: array
          items:
            type: string
            format: uuid
        contactIds:
          type: array
          items:
            type: string
            format: uuid
        dealIds:
          type: array
          items:
            type: string
            format: uuid
        assignedToIds:
          type: array
          items:
            type: string
            format: uuid
        externalId:
          type: string
        source:
          type: string

    TaskUpdate:
      type: object
      description: |
        All fields are optional. Array fields (`contactIds`, `companyIds`, `dealIds`, `assignedToIds`) replace the existing array entirely on update.
      properties:
        name:
          type: string
        content:
          type: object
          description: Replaces the entire task content. Must be a valid Tiptap document.
        description:
          type: object
          description: Replaces the entire description. Must be a valid Tiptap document.
        done:
          type: boolean
        priority:
          type: integer
          nullable: true
          enum: [null, 1, 2, 3, 4]
        deadline:
          type: string
          format: date-time
          nullable: true
        companyIds:
          type: array
          items:
            type: string
            format: uuid
        contactIds:
          type: array
          items:
            type: string
            format: uuid
        dealIds:
          type: array
          items:
            type: string
            format: uuid
        assignedToIds:
          type: array
          items:
            type: string
            format: uuid
        externalId:
          type: string
        source:
          type: string

    Pipeline:
      type: object
      description: A sales pipeline that contains stages and deals.
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        icon:
          type: string
          nullable: true
          description: Icon identifier for the pipeline
        color:
          type: string
          nullable: true
          description: Hex color code (e.g. `#2554F0`)
        confidenceEnabled:
          type: boolean
          description: Whether deal confidence scores are enabled for this pipeline
        defaultStage:
          type: string
          format: uuid
          nullable: true
          description: The stage ID new deals are assigned to by default
        order:
          type: integer
          description: Display order among pipelines
        archived:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    PipelineCreate:
      type: object
      required:
        - workspaceId
        - name
      properties:
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        icon:
          type: string
        color:
          type: string
          description: Hex color code
        confidenceEnabled:
          type: boolean
          default: false
        defaultStage:
          type: string
          format: uuid
          nullable: true
        order:
          type: integer

    PipelineUpdate:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        icon:
          type: string
        color:
          type: string
        confidenceEnabled:
          type: boolean
        defaultStage:
          type: string
          format: uuid
          nullable: true
        order:
          type: integer

    PipelineStage:
      type: object
      description: A stage within a pipeline. Deals move through stages as they progress.
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        pipelineId:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        type:
          type: string
          nullable: true
          description: |
            Semantic type of the stage.
            - `lead` — entry stage
            - `in-progress` — active stage
            - `won` — terminal success stage
            - `lost` — terminal loss stage
            - `null` — unclassified
          enum: [lead, in-progress, won, lost, null]
        confidence:
          type: string
          nullable: true
          description: Win probability for deals in this stage (0.0–1.0). Only meaningful when `confidenceEnabled` is true on the pipeline.
          example: "0.70"
        icon:
          type: string
          nullable: true
        color:
          type: string
          nullable: true
          description: Hex color code
        order:
          type: integer
          description: Display order within the pipeline (ascending)
        archived:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    PipelineStageCreate:
      type: object
      required:
        - workspaceId
        - pipelineId
        - name
      properties:
        workspaceId:
          type: string
          format: uuid
        pipelineId:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        type:
          type: string
          nullable: true
          enum: [lead, in-progress, won, lost]
        confidence:
          type: number
          minimum: 0
          maximum: 1
          description: Win probability (0.0–1.0)
        icon:
          type: string
        color:
          type: string
        order:
          type: integer

    PipelineStageUpdate:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        type:
          type: string
          nullable: true
          enum: [lead, in-progress, won, lost]
        confidence:
          type: number
          minimum: 0
          maximum: 1
        icon:
          type: string
        color:
          type: string
        order:
          type: integer

    Workspace:
      type: object
      description: |
        A workspace is the top-level organizational unit in Zero. All records (companies, contacts, deals, etc.) belong to a workspace.
        Creation of workspaces via the API is not supported.
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        key:
          type: string
          description: URL-safe identifier for the workspace (e.g. `my-company`)
        domain:
          type: string
          nullable: true
          description: Primary domain associated with the workspace
        domains:
          type: array
          description: List of email domains associated with the workspace
          items:
            type: object
            properties:
              domain:
                type: string
        avatar:
          type: string
          nullable: true
          description: URL of the workspace logo/avatar
        color:
          type: string
          nullable: true
        type:
          type: string
          nullable: true
          description: Workspace type (e.g. `internal`)
        trialEndsAt:
          type: string
          format: date-time
          nullable: true
        settings:
          type: object
          description: |
            Workspace configuration. Includes `timezone`, `currency`, `contactTypes`, and `recordColumnConfigs` (column display settings per entity type).
        featureFlags:
          type: object
          description: Feature flag overrides for this workspace
        archived:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    WorkspaceUpdate:
      type: object
      properties:
        name:
          type: string
        domain:
          type: string
          nullable: true
        avatar:
          type: string
          nullable: true
        color:
          type: string
          nullable: true
        settings:
          type: object
          description: Replaces the entire settings object. Include all desired settings, not just changed fields.

    CalendarEvent:
      type: object
      description: |
        A meeting (calendar event). Meetings recorded by a connected notetaker integration also have transcript segments, retrievable via `GET /api/calendarEventTranscripts` filtered by `calendarEventId`.

        Only a curated, read-only subset of meeting fields is exposed via the API.
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
          nullable: true
          description: Title of the meeting.
        emoji:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        location:
          type: string
          nullable: true
        meetingLink:
          type: string
          nullable: true
          description: Video conferencing link (e.g. Google Meet, Zoom).
        startTime:
          type: string
          format: date-time
          nullable: true
          description: Start of the meeting. Null for all-day events, which use `startDate`/`endDate` instead.
        endTime:
          type: string
          format: date-time
          nullable: true
          description: End of the meeting.
        startDate:
          type: string
          format: date
          nullable: true
          description: Start date for all-day events.
        endDate:
          type: string
          format: date
          nullable: true
          description: End date for all-day events.
        busy:
          type: boolean
          description: Whether the meeting marks attendees as busy.
        organizer:
          type: object
          nullable: true
          description: The meeting organizer, as provided by the calendar source. May include additional provider-specific fields.
          properties:
            email:
              type: string
              nullable: true
            displayName:
              type: string
              nullable: true
              description: Display name of the organizer.
        attendeeEmails:
          type: array
          nullable: true
          items:
            type: string
          description: Email addresses of the meeting attendees.
        summary:
          type: object
          nullable: true
          description: |
            AI-generated meeting summary in [Tiptap](https://tiptap.dev/) document format (a JSON object with a `type: "doc"` root). Null when no summary has been generated.
        userIds:
          type: array
          items:
            type: string
            format: uuid
          description: Zero users associated with the meeting.
        calendarIds:
          type: array
          items:
            type: string
            format: uuid
          description: Calendars the meeting belongs to.
        companyIds:
          type: array
          items:
            type: string
            format: uuid
          description: Companies linked to the meeting.
        contactIds:
          type: array
          items:
            type: string
            format: uuid
          description: Contacts linked to the meeting.
        dealIds:
          type: array
          items:
            type: string
            format: uuid
          description: Deals linked to the meeting.
        external:
          type: boolean
          description: Whether the meeting originated from an external calendar sync.
        externalNotetakerUrl:
          type: string
          nullable: true
          description: Link to the meeting in the external notetaker (e.g. Circleback).
        audioUrl:
          type: string
          nullable: true
          description: URL to the meeting audio recording, if available.
        videoUrl:
          type: string
          nullable: true
          description: URL to the meeting video recording, if available.
        archived:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    Transcript:
      type: object
      description: |
        A single transcript segment (one speaker turn) from a recorded meeting. A meeting's full transcript is made up of many segments that share the same `calendarEventId`, ordered by `startTime`.
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        calendarEventId:
          type: string
          format: uuid
          description: The meeting (calendar event) this segment belongs to.
        integrationId:
          type: string
          format: uuid
          nullable: true
          description: The notetaker integration that produced this segment.
        speaker:
          type: string
          nullable: true
          description: Name of the speaker for this segment (e.g. "Jane Doe"), or "Unknown".
        content:
          type: string
          nullable: true
          description: The spoken text of this segment.
        startTime:
          type: integer
          nullable: true
          description: Start offset of this segment, in milliseconds from the start of the meeting.
        endTime:
          type: integer
          nullable: true
          description: End offset of this segment, in milliseconds from the start of the meeting.
        duration:
          type: integer
          nullable: true
          description: Length of this segment in milliseconds (`endTime` − `startTime`).
        topic:
          type: string
          nullable: true
          description: Optional topic label for the segment. Often empty.
        sentiment:
          type: string
          nullable: true
          description: Optional sentiment label for the segment. Often empty.
        archived:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        createdById:
          type: string
          format: uuid
          nullable: true
        updatedById:
          type: string
          format: uuid
          nullable: true

    User:
      type: object
      description: |
        A user in the Zero system. The `GET /api/users` endpoint returns all users accessible to the authenticated token.
        To find members of a specific workspace, use `GET /api/memberships` filtered by `workspaceId`, then resolve user details via `GET /api/users?where={"id":{"$in":[...]}}`.
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          nullable: true
        email:
          type: string
          format: email
        title:
          type: string
          nullable: true
          description: Job title
        avatar:
          type: string
          nullable: true
          description: URL of the user's profile picture
        linkedin:
          type: string
          nullable: true
          description: LinkedIn username
        active:
          type: boolean
          description: Whether the user account is active
        emailConfirmed:
          type: boolean
        lastSeenAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    Membership:
      type: object
      description: Links a user to a workspace with a role.
      properties:
        id:
          type: string
          format: uuid
        userId:
          type: string
          format: uuid
          description: The ID of the user
        workspaceId:
          type: string
          format: uuid
          description: The ID of the workspace
        role:
          type: string
          enum: [admin, member]
          description: The user's role within the workspace
        invitedById:
          type: string
          format: uuid
          nullable: true
          description: ID of the user who sent the invite
        inviteMessage:
          type: string
          nullable: true
        archived:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    List:
      type: object
      description: |
        A list groups records of a single entity type (contacts, companies, or deals).

        - **Regular lists** (`filterBased: false`) — records are explicitly added via the `listIds` field on contact/company/deal PATCH.
        - **Dynamic lists** (`filterBased: true`) — records are not stored. The list shows all records matching the filters defined on its views.

        Every list must have an `icon` set to display correctly in the UI.
        Every list must have at least one view to be usable in the UI.
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
        entity:
          type: string
          enum: [contacts, companies, deals]
          description: The type of records this list contains
        filterBased:
          type: boolean
          description: "`true` for dynamic lists, `false` for regular lists"
        filters:
          type: object
          description: |
            Should be left empty (`{}`). Filters for dynamic lists belong on the **views**, not on the list itself.
            The API will accept and store values here, but the UI uses view-level filters to determine which records are shown — not this field.
        icon:
          type: string
          nullable: true
          description: |
            Icon name from the Zero icon set. See the `ListCreate` schema for the full list of valid values.
            Required — lists without an icon do not display correctly in the UI.
        color:
          type: string
          nullable: true
          description: Hex color code (e.g. `"#8B5CF6"`)
        order:
          type: integer
          nullable: true
        listFolderId:
          type: string
          format: uuid
          nullable: true
          description: ID of the folder this list belongs to, if any
        archived:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    ListCreate:
      type: object
      required:
        - workspaceId
        - name
        - entity
        - icon
      properties:
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
        entity:
          type: string
          enum: [contacts, companies, deals]
        filterBased:
          type: boolean
          default: false
          description: "`true` for dynamic list, `false` for regular list"
        color:
          type: string
          description: Hex color code (e.g. `"#8B5CF6"`)
        icon:
          type: string
          description: Required. Icon name from the Zero icon set.
          enum:
            - "access-point"
            - "activity-heartbeat"
            - "adjustments-alt"
            - "adjustments-horizontal"
            - "affiliate"
            - "alarm"
            - "alert-hexagon"
            - "alert-triangle"
            - "analyze"
            - "antenna-bars-5"
            - "aperture"
            - "api-app"
            - "archery-arrow"
            - "archive"
            - "armchair"
            - "arrow-back-up"
            - "arrow-bar-to-down"
            - "arrow-bar-to-left"
            - "arrow-bar-to-right"
            - "arrow-bar-to-up"
            - "arrow-bear-right"
            - "arrow-big-down"
            - "arrow-big-left"
            - "arrow-bounce"
            - "arrow-down-circle"
            - "arrow-down-left-circle"
            - "arrow-down-right-circle"
            - "arrow-forward-up"
            - "arrow-left-circle"
            - "arrow-narrow-left"
            - "arrow-narrow-right"
            - "arrow-right-circle"
            - "arrow-up-circle"
            - "arrow-up-left-circle"
            - "arrow-up-right-circle"
            - "arrows-diagonal"
            - "arrows-diagonal-2"
            - "arrows-double-ne-sw"
            - "arrows-double-nw-se"
            - "arrows-double-se-nw"
            - "arrows-double-sw-ne"
            - "arrows-exchange"
            - "arrows-horizontal"
            - "arrows-join"
            - "arrows-join-2"
            - "arrows-left-down"
            - "arrows-left-right"
            - "arrows-maximize"
            - "arrows-minimize"
            - "arrows-right-down"
            - "arrows-right-left"
            - "arrows-sort"
            - "arrows-split"
            - "arrows-split-2"
            - "arrows-transfer-down"
            - "arrows-transfer-up"
            - "arrows-vertical"
            - "artboard"
            - "artboard-off"
            - "aspect-ratio"
            - "assembly"
            - "asset"
            - "asterisk"
            - "asterisk-simple"
            - "at"
            - "at-off"
            - "atom"
            - "augmented-reality"
            - "automatic-gearbox"
            - "award"
            - "award-off"
            - "axe"
            - "axis-x"
            - "axis-y"
            - "baby-carriage"
            - "backhoe"
            - "backpack"
            - "badge"
            - "badges"
            - "ball-american-football"
            - "ball-baseball"
            - "ball-basketball"
            - "ball-bowling"
            - "ball-football"
            - "ball-tennis"
            - "ball-volleyball"
            - "balloon"
            - "balloon-off"
            - "ballpen"
            - "ballpen-off"
            - "bandage"
            - "barbell"
            - "barcode"
            - "barrel"
            - "barrier-block"
            - "baseline"
            - "basket"
            - "basket-off"
            - "bath"
            - "battery"
            - "battery-charging"
            - "battery-off"
            - "beach"
            - "bell"
            - "bike"
            - "binoculars"
            - "bleach"
            - "blob"
            - "blur"
            - "bolt"
            - "bookmark"
            - "border-all"
            - "border-corners"
            - "bowl-chopsticks"
            - "box"
            - "box-model-2"
            - "box-multiple"
            - "brackets-angle"
            - "brain"
            - "briefcase"
            - "brightness-down"
            - "broadcast"
            - "brush"
            - "bug"
            - "building"
            - "building-bank"
            - "building-castle"
            - "bulb"
            - "cactus"
            - "calculator"
            - "calendar"
            - "camera"
            - "capture"
            - "car"
            - "car-fan"
            - "carambola"
            - "cardboards"
            - "cash-banknote"
            - "chart-bar"
            - "chart-funnel"
            - "chart-line"
            - "chart-pie"
            - "check"
            - "chess-king"
            - "chess-rook"
            - "chevron-down"
            - "chevron-down-left"
            - "chevron-down-right"
            - "chevron-left"
            - "chevron-right"
            - "chevron-up"
            - "chevron-up-left"
            - "chevron-up-right"
            - "chevrons-down"
            - "chevrons-down-left"
            - "chevrons-down-right"
            - "chevrons-left"
            - "chevrons-right"
            - "chevrons-up"
            - "chevrons-up-left"
            - "chevrons-up-right"
            - "circle"
            - "clipboard"
            - "clock"
            - "cloud"
            - "code"
            - "coffee"
            - "coin"
            - "compass"
            - "confetti"
            - "copy"
            - "crane"
            - "credit-card"
            - "crown"
            - "currency-dollar"
            - "currency-euro"
            - "cut"
            - "cylinder"
            - "dashboard"
            - "database"
            - "details"
            - "device-desktop"
            - "device-gamepad-2"
            - "diamond"
            - "diamonds"
            - "direction-sign"
            - "disc"
            - "discount"
            - "dna"
            - "droplet"
            - "ear"
            - "edit"
            - "exchange"
            - "eye"
            - "eyeglass"
            - "feather"
            - "file"
            - "file-text"
            - "filter"
            - "fingerprint"
            - "flag"
            - "flag-3"
            - "flame"
            - "flask"
            - "flower"
            - "focus-2"
            - "folder"
            - "forbid-2"
            - "frustum"
            - "gauge"
            - "geometry"
            - "gift"
            - "glass-cocktail"
            - "golf"
            - "graph"
            - "hand-finger"
            - "hand-love-you"
            - "headphones"
            - "heart"
            - "hexagonal-pyramid"
            - "hierarchy"
            - "home"
            - "hourglass-empty"
            - "jacket"
            - "key"
            - "keyframe"
            - "keyframe-align-horizontal"
            - "keyframes"
            - "label"
            - "laurel-wreath"
            - "layers-subtract"
            - "layout-board"
            - "layout-cards"
            - "layout-grid"
            - "leaf"
            - "lemon"
            - "license"
            - "link"
            - "location"
            - "lock-open"
            - "mail"
            - "mailbox"
            - "map-pin"
            - "mask"
            - "medical-cross"
            - "message-chatbot"
            - "message-dots"
            - "microphone"
            - "moneybag"
            - "mood-empty"
            - "mood-happy"
            - "mood-sad"
            - "mood-share"
            - "mood-smile"
            - "moon"
            - "mouse-2"
            - "mug"
            - "music"
            - "nut"
            - "paperclip"
            - "phone"
            - "plane-tilt"
            - "plant"
            - "plus-circle"
            - "pointer"
            - "presentation"
            - "rainbow"
            - "road"
            - "rocket"
            - "rosette-discount-check"
            - "rotate-dot"
            - "rubber-stamp"
            - "run"
            - "scale"
            - "scissors"
            - "search"
            - "send"
            - "settings"
            - "shield"
            - "shopping-cart"
            - "sparkles"
            - "speakerphone"
            - "spiral"
            - "star"
            - "sun"
            - "tag"
            - "tags"
            - "target"
            - "thumb-down"
            - "thumb-up"
            - "timeline-event"
            - "toggle-left"
            - "tool"
            - "trash"
            - "trending-up"
            - "triangle-inverted"
            - "trophy"
            - "umbrella"
            - "upload"
            - "user"
            - "users"
            - "video"
            - "volume"
            - "volume-2"
            - "wallet"
            - "webhook"
            - "wifi"
            - "windmill"
            - "world"
            - "x"
        order:
          type: integer
        listFolderId:
          type: string
          format: uuid
          nullable: true

    ListUpdate:
      type: object
      description: All fields are optional. Multiple fields can be updated in a single PATCH request.
      properties:
        name:
          type: string
        # entity is intentionally excluded — entity type cannot be changed after creation.
        color:
          type: string
        icon:
          type: string
        filterBased:
          type: boolean
          description: Convert between regular and dynamic list. When switching to `true`, set filters on each view rather than on the list itself.
        filters:
          type: object
          description: |
            The API accepts and stores values here, but the UI uses view-level filters to determine which records are shown.
            Recommended to leave as `{}` and set filters on views instead.
        order:
          type: integer
        listFolderId:
          type: string
          format: uuid
          nullable: true

    View:
      type: object
      description: |
        A view belongs to a list and defines filters and display configuration. A list can have multiple views, each with its own independent set of filters.

        For dynamic lists, the view filters determine which workspace records are shown.
        For regular lists, filters narrow down the explicitly-added records.
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        listId:
          type: string
          format: uuid
        name:
          type: string
        entity:
          type: string
          enum: [contacts, companies, deals]
        type:
          type: string
          enum: [grid, board]
          description: |
            View display type.
            - `grid` — flat table layout.
            - `board` — kanban-style layout. `settings.groupBy` should be set for meaningful grouping (the API does not enforce this but the UI needs it to display correctly).
            To convert a `grid` view to `board`, PATCH `{"type": "board"}`.
        filters:
          type: object
          description: |
            Filters that determine which records are shown in this view. Uses the same operator syntax as the `where` query parameter.

            For dynamic lists, this is the primary (and only) mechanism for filtering records — do not set filters on the list object itself.

            Example:
            ```json
            {
              "stage": { "$in": ["<uuid>", "<uuid>"] },
              "lastActivity.time": { "$lt": "-7d" },
              "custom.<column-id>": "some-value"
            }
            ```
        settings:
          type: object
          description: |
            Display settings for the view.

            ```json
            {
              "columns": [
                { "key": "&" },
                { "key": "value" },
                { "key": "closeDate" },
                { "key": "custom.<column-id>" }
              ],
              "groupBy": "stage",
              "isDefault": true
            }
            ```

            - `columns` — ordered list of visible columns. `"&"` is the primary name column and should always be first.
            - `groupBy` — field key to group records by. Required for `board` views. Accepts standard field names (e.g. `"stage"`) or `"custom.<column-id>"`.
            - `isDefault` — if `true`, this view is opened by default when navigating to the list. Only one view per list should have this set.

            When PATCHing `settings`, always include the full object — partial updates will overwrite existing column configuration.
        highlights:
          type: array
          items: {}
          description: Highlight rules for the view
        order:
          type: integer
        archived:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    ViewCreate:
      type: object
      required:
        - workspaceId
        - listId
        - name
        - entity
        - type
      properties:
        workspaceId:
          type: string
          format: uuid
        listId:
          type: string
          format: uuid
        name:
          type: string
        entity:
          type: string
          enum: [contacts, companies, deals]
        type:
          type: string
          enum: [grid, board]
          description: |
            View type. Use `grid` for table layout, `board` for kanban.
            Note: you can also POST directly with `type: "board"` — the API accepts it. The create-as-grid-then-PATCH approach also works.
        filters:
          type: object
          description: Initial filters for the view. For dynamic lists, this controls which records are shown.
        settings:
          type: object
          description: |
            Initial display settings. If omitted, defaults to `{}` (no columns configured).
            Should include `columns` array and optionally `groupBy` and `isDefault`.
            ```json
            {
              "columns": [{ "key": "&" }, { "key": "value" }],
              "groupBy": null,
              "isDefault": true
            }
            ```
        order:
          type: integer
          description: Display order of this view within the list. Use `0` for the first/default view.

    ViewUpdate:
      type: object
      description: All fields are optional. Multiple fields can be updated in a single PATCH request.
      properties:
        name:
          type: string
          description: Display name of the view.
        type:
          type: string
          enum: [grid, board]
          description: |
            Change the view layout. Use `"board"` for kanban, `"grid"` for table.
            Can be toggled freely — both `grid→board` and `board→grid` work.
            When switching to `"board"`, ensure `settings.groupBy` is set for meaningful column grouping.
        # entity is intentionally excluded — entity type cannot be changed after creation.
        filters:
          type: object
          description: |
            Replaces the **entire** filters object — include all desired filters, not just the changed ones.
            For dynamic lists this controls which records are shown.
            Set to `{}` to clear all filters.
        settings:
          type: object
          description: |
            Replaces the **entire** settings object — always include the full `columns` array, `groupBy`, and `isDefault` values.
            Partial settings updates will overwrite existing column configuration.
            If `settings` is omitted on creation, it defaults to `{}`.
        order:
          type: integer
          description: Display order of this view within the list.

    CustomActivity:
      type: object
      description: A logged activity (call, meeting, or custom interaction) linked to companies, contacts, and deals.
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
          description: Title or summary of the activity
        typeId:
          type: string
          format: uuid
          nullable: true
          description: ID of the custom activity type (e.g. Call, Meeting). Use `GET /api/customActivityTypes` to discover available types.
        type:
          type: string
          nullable: true
          description: Legacy type string. Prefer using `typeId` for new activities.
        time:
          type: string
          format: date-time
          nullable: true
          description: When the activity occurred (ISO 8601)
        content:
          type: object
          nullable: true
          description: Flexible JSONB object for structured activity data (e.g. call notes, meeting agenda).
        custom:
          type: object
          nullable: true
          description: |
            Custom property values defined by the activity type's `customFields`. Keyed by the custom field `id`.

            For example, if the Call type has a custom field with `"id": "call-result"`, you can store:
            ```json
            {"call-result": "connected"}
            ```

            When updating via PATCH, use dot-notation (e.g. `"custom.call-result": "connected"`) to avoid overwriting other custom properties.
        userId:
          type: string
          format: uuid
          nullable: true
          description: The user who performed the activity. Auto-set from the authenticated user on creation if not provided.
        userIds:
          type: array
          items:
            type: string
            format: uuid
          description: Multiple users associated with the activity
        companyIds:
          type: array
          items:
            type: string
            format: uuid
          description: Companies linked to this activity
        contactIds:
          type: array
          items:
            type: string
            format: uuid
          description: Contacts linked to this activity
        dealIds:
          type: array
          items:
            type: string
            format: uuid
          description: Deals linked to this activity
        archived:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        createdById:
          type: string
          format: uuid
          nullable: true
        externalId:
          type: string
          nullable: true
          description: ID from external system for integrations
        source:
          type: string
          nullable: true

    CustomActivityCreate:
      type: object
      required:
        - workspaceId
      properties:
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
          description: Title or summary of the activity
        typeId:
          type: string
          format: uuid
          description: ID of the custom activity type. Use `GET /api/customActivityTypes` to discover available types.
        time:
          type: string
          format: date-time
          description: When the activity occurred. Defaults to now if not provided.
        content:
          type: object
          description: Flexible JSONB object for structured activity data.
        custom:
          type: object
          description: Custom property values keyed by the activity type's custom field IDs.
        userId:
          type: string
          format: uuid
          description: The user who performed the activity. Auto-set from the authenticated user if not provided.
        userIds:
          type: array
          items:
            type: string
            format: uuid
        companyIds:
          type: array
          items:
            type: string
            format: uuid
        contactIds:
          type: array
          items:
            type: string
            format: uuid
        dealIds:
          type: array
          items:
            type: string
            format: uuid
        externalId:
          type: string
        source:
          type: string

    CustomActivityUpdate:
      type: object
      description: |
        All fields are optional. Array fields (`contactIds`, `companyIds`, `dealIds`, `userIds`) replace the existing array entirely on update.
      properties:
        name:
          type: string
        typeId:
          type: string
          format: uuid
        time:
          type: string
          format: date-time
        content:
          type: object
        custom:
          type: object
        userId:
          type: string
          format: uuid
        userIds:
          type: array
          items:
            type: string
            format: uuid
        companyIds:
          type: array
          items:
            type: string
            format: uuid
        contactIds:
          type: array
          items:
            type: string
            format: uuid
        dealIds:
          type: array
          items:
            type: string
            format: uuid
        externalId:
          type: string
        source:
          type: string

    CustomActivityType:
      type: object
      description: |
        A definition for a type of activity that can be logged in a workspace (e.g. Call, Meeting, Site Visit).

        Each type can define custom fields that appear when logging an activity. Default types (Call, Meeting) are created automatically for new workspaces.
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
          description: Display name of the activity type (e.g. "Call", "Meeting")
        icon:
          type: string
          nullable: true
          description: Icon identifier (e.g. "phone", "calendar", "building")
        color:
          type: string
          nullable: true
          description: Hex color code for UI display (e.g. "#C48E1C")
        shortcut:
          type: string
          nullable: true
          description: Keyboard shortcut key for quick activity logging (e.g. "K" for Call, "M" for Meeting)
        description:
          type: string
          nullable: true
          description: Human-readable description of the activity type
        customFields:
          type: array
          description: |
            Custom field definitions for this activity type. When creating an activity with this type, the `custom` object on the activity can contain values keyed by these field IDs.
          items:
            type: object
            properties:
              id:
                type: string
                description: Unique identifier for the custom field. Used as the key in the activity's `custom` object.
              name:
                type: string
                description: Human-readable field name
              type:
                type: string
                description: Field type (e.g. "select", "text", "number")
              options:
                type: array
                description: Available options for select-type fields
                items:
                  type: object
                  properties:
                    key:
                      type: string
                      description: The value stored on the activity
                    name:
                      type: string
                      description: Human-readable option name
                    color:
                      type: string
                      description: Hex color code for UI display
        archived:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        createdById:
          type: string
          format: uuid
          nullable: true

    CustomActivityTypeCreate:
      type: object
      required:
        - workspaceId
        - name
      properties:
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
          description: Display name of the activity type
        icon:
          type: string
          description: Icon identifier
        color:
          type: string
          description: Hex color code
        shortcut:
          type: string
          description: Keyboard shortcut key
        description:
          type: string
        customFields:
          type: array
          description: Custom field definitions for this activity type
          items:
            type: object
            required:
              - id
              - name
              - type
            properties:
              id:
                type: string
              name:
                type: string
              type:
                type: string
              options:
                type: array
                items:
                  type: object
                  properties:
                    key:
                      type: string
                    name:
                      type: string
                    color:
                      type: string

    CustomActivityTypeUpdate:
      type: object
      description: |
        All fields are optional. The `customFields` array replaces the existing array entirely on update — always include all desired fields.
      properties:
        name:
          type: string
        icon:
          type: string
        color:
          type: string
        shortcut:
          type: string
        description:
          type: string
        customFields:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              type:
                type: string
              options:
                type: array
                items:
                  type: object
                  properties:
                    key:
                      type: string
                    name:
                      type: string
                    color:
                      type: string

    File:
      type: object
      description: |
        A file or attachment uploaded to a workspace. Created via `POST /api/files/upload` (multipart/form-data).
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        companyId:
          type: string
          format: uuid
          nullable: true
          description: Company this file is attached to, if any.
        contactId:
          type: string
          format: uuid
          nullable: true
          description: Contact this file is attached to, if any.
        dealId:
          type: string
          format: uuid
          nullable: true
          description: Deal this file is attached to, if any.
        noteId:
          type: string
          format: uuid
          nullable: true
          description: Note this file is attached to, if any.
        calendarEventId:
          type: string
          format: uuid
          nullable: true
        placement:
          type: string
          nullable: true
          description: Optional placement/context label for the file.
        url:
          type: string
          nullable: true
          description: |
            Storage URL for the file. For `type=file` uploads this points to authenticated storage and is not directly accessible — use `GET /api/files/download` to get a signed URL.
        thumbnailUrl:
          type: string
          nullable: true
          description: Generated thumbnail image URL, or `null` if the content type does not support previews.
        previewUrl:
          type: string
          nullable: true
          description: Generated preview image URL, or `null` if the content type does not support previews.
        name:
          type: string
          nullable: true
          description: Original filename.
        description:
          type: string
          nullable: true
        size:
          type: integer
          nullable: true
          description: File size in bytes.
        type:
          type: string
          nullable: true
          description: MIME type of the file (e.g. `application/pdf`, `image/png`).
        metadata:
          type: object
          nullable: true
          description: Media metadata extracted on upload.
          properties:
            width:
              type: integer
            height:
              type: integer
            type:
              type: string
            format:
              type: string
            pages:
              type: integer
              description: Number of pages (for multi-page documents such as PDFs).
            duration:
              type: number
              description: Duration in seconds (for audio/video).
        assetId:
          type: string
          nullable: true
          description: Cloudinary asset ID.
        publicId:
          type: string
          nullable: true
          description: Cloudinary public ID.
        googleCloudStorageId:
          type: string
          nullable: true
          description: Internal storage key. Use `GET /api/files/download` rather than referencing this directly.
        externalId:
          type: string
          nullable: true
          description: ID from an external system for integrations.
        importId:
          type: string
          format: uuid
          nullable: true
        archived:
          type: boolean
        archivedById:
          type: string
          format: uuid
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        createdById:
          type: string
          format: uuid
          nullable: true
        updatedById:
          type: string
          format: uuid
          nullable: true

    FileUpdate:
      type: object
      description: Editable file metadata. The file's binary contents cannot be changed via PATCH — re-upload to replace them.
      properties:
        name:
          type: string
        description:
          type: string
        placement:
          type: string
        companyId:
          type: string
          format: uuid
        contactId:
          type: string
          format: uuid
        dealId:
          type: string
          format: uuid
        noteId:
          type: string
          format: uuid
        externalId:
          type: string

    Sequence:
      type: object
      description: A multi-step outreach sequence (cadence).
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        name:
          type: string
          nullable: true
        status:
          type: string
          nullable: true
          enum: [draft, active, paused, archived]
          description: |
            Lifecycle state. Transition with `/activate` and `/pause` rather than patching this directly.
        listId:
          type: string
          format: uuid
          nullable: true
          description: The sequence's **source list**. Add this list to a contact's `listIds` to enroll the contact.
        mailboxId:
          type: string
          format: uuid
          nullable: true
          description: Default sending mailbox for the sequence's email steps.
        linkedinAccountId:
          type: string
          format: uuid
          nullable: true
        sequenceFolderId:
          type: string
          format: uuid
          nullable: true
        icon:
          type: string
          nullable: true
        color:
          type: string
          nullable: true
        order:
          type: integer
          nullable: true
        settings:
          type: object
          description: Sequence-level settings (JSON).
        archived:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        createdById:
          type: string
          format: uuid
          nullable: true
        updatedById:
          type: string
          format: uuid
          nullable: true

    SequenceUpdate:
      type: object
      description: |
        Editable sequence settings. To rename use `POST /api/sequences/rename`; to change status use `/activate` or `/pause`.
      properties:
        mailboxId:
          type: string
          format: uuid
          nullable: true
          description: Default sending mailbox for email steps.
        linkedinAccountId:
          type: string
          format: uuid
          nullable: true
        sequenceFolderId:
          type: string
          format: uuid
          nullable: true
        icon:
          type: string
        color:
          type: string
        order:
          type: integer
        settings:
          type: object

    SequenceStep:
      type: object
      description: A single step within a sequence.
      properties:
        id:
          type: string
          format: uuid
        sequenceId:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        type:
          type: string
          nullable: true
          enum:
            - startSequence
            - sendEmail
            - sendLinkedinMessage
            - sendLinkedinConnectionRequest
            - addTask
            - endSequence
            - delay
            - startAutomation
            - aiCompose
          description: |
            Step type. `startSequence` (order 0) and `aiCompose` are managed automatically; the rest are added via `POST /api/sequenceSteps`.
        name:
          type: string
          nullable: true
        subject:
          type: string
          nullable: true
          description: Email subject line (for `sendEmail` steps).
        content:
          type: object
          description: Step body. For `sendEmail` steps this is a Tiptap document.
        settings:
          type: object
          description: |
            Per-type configuration. For email steps this includes `mailboxId`, `subject`, `content`, `draft`, `stopOnReply`, and an optional `replyToStepId` (threads the email as a reply to an earlier step's send).
        triggerSettings:
          type: object
          description: Timing configuration. For `delay` steps, `delayMs` is the wait in milliseconds.
        templateId:
          type: string
          format: uuid
          nullable: true
        order:
          type: integer
          nullable: true
        archived:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        createdById:
          type: string
          format: uuid
          nullable: true
        updatedById:
          type: string
          format: uuid
          nullable: true

    SequenceStepCreate:
      type: object
      required:
        - workspaceId
        - sequenceId
        - type
      properties:
        workspaceId:
          type: string
          format: uuid
        sequenceId:
          type: string
          format: uuid
        type:
          type: string
          enum:
            - sendEmail
            - sendLinkedinMessage
            - sendLinkedinConnectionRequest
            - addTask
            - endSequence
            - delay
            - startAutomation
        name:
          type: string
        subject:
          type: string
          description: Email subject (for `sendEmail` steps).
        content:
          type: object
          description: Step body — a Tiptap document for `sendEmail` steps.
        settings:
          type: object
          description: Per-type configuration. The fields depend on `type` — see the endpoint description for the full per-type reference (e.g. `mailboxId`/`subject`/`content` for `sendEmail`, `userId` for LinkedIn steps, `automationId` for `startAutomation`).
        triggerSettings:
          type: object
          description: Timing configuration. Used by `delay` steps (`delayMs`); most other types leave this empty. See the endpoint description.
        templateId:
          type: string
          format: uuid
        order:
          type: integer
          description: Position after the start step (which is order 0).

    SequenceStepUpdate:
      type: object
      properties:
        type:
          type: string
          enum:
            - sendEmail
            - sendLinkedinMessage
            - sendLinkedinConnectionRequest
            - addTask
            - endSequence
            - delay
            - startAutomation
          description: |
            Changing a step's type is allowed but uncommon. The auto-managed `startSequence` and `aiCompose` types are intentionally excluded (matching `SequenceStepCreate`).
        name:
          type: string
        subject:
          type: string
        content:
          type: object
        settings:
          type: object
        triggerSettings:
          type: object
        templateId:
          type: string
          format: uuid
        order:
          type: integer

    SequenceContactStatus:
      type: object
      description: |
        An enrollment — one contact's progress through one sequence. Created automatically when a contact is added to the sequence's source list.
      properties:
        id:
          type: string
          format: uuid
        workspaceId:
          type: string
          format: uuid
        sequenceId:
          type: string
          format: uuid
        listId:
          type: string
          format: uuid
          description: The sequence source list the contact was enrolled through.
        contactId:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - waitingForApproval
            - waitingForPickUp
            - active
            - paused
            - completed
            - stopped
            - cancelled
          description: |
            `waitingForPickUp` — enrolled, awaiting the background worker.
            `waitingForApproval` — manual roll-out, awaiting approval in the app.
            `active` — in flight. `paused` — sequence paused. `completed` — reached the end.
            `stopped` — ended early (e.g. replied). `cancelled` — removed from the source list.
        outcomeReason:
          type: string
          nullable: true
          enum:
            - responded
            - positiveResponse
            - negativeResponse
            - automatedResponse
            - bounced
            - unsubscribed
            - error
        outcomeStepId:
          type: string
          format: uuid
          nullable: true
          description: The step the contact was on when the outcome was recorded.
        responseCategorisation:
          type: string
          nullable: true
          enum: [positive, negative, automated]
          description: How a reply from the contact was categorised, if any.
        errorMessage:
          type: string
          nullable: true
          description: Populated when the enrollment hit an error.
        currentStepId:
          type: string
          format: uuid
          nullable: true
        waitingOn:
          type: string
          nullable: true
          enum:
            - delay
            - emailScheduling
            - emailSent
            - linkedinMessageSent
            - linkedinConnectionRequestSent
            - linkedinConnectionResponse
            - sequenceTaskDeadline
            - sequenceTaskDone
        lastCompletedStepId:
          type: string
          format: uuid
          nullable: true
        lastCompletedStepName:
          type: string
          nullable: true
        nextActionAt:
          type: string
          format: date-time
          nullable: true
        startedAt:
          type: string
          format: date-time
          nullable: true
        endedAt:
          type: string
          format: date-time
          nullable: true
        outcomeAt:
          type: string
          format: date-time
          nullable: true
        lastUpdatedAt:
          type: string
          format: date-time
          description: When the enrollment's state last changed.
        archived:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    SequenceProgressSummaryEntry:
      type: object
      description: Aggregate enrollment metrics for a single sequence.
      properties:
        sequenceId:
          type: string
          format: uuid
        enrolled:
          type: integer
          description: Total non-archived enrollments.
        waitingForPickUp:
          type: integer
        waitingForApproval:
          type: integer
        active:
          type: integer
        paused:
          type: integer
        statusCompleted:
          type: integer
          description: Enrollments that reached the end of the sequence.
        stopped:
          type: integer
        cancelled:
          type: integer
        replied:
          type: integer
        bounced:
          type: integer
        unsubscribed:
          type: integer
        error:
          type: integer
          description: Enrollments whose outcome was specifically `error` (delivery/processing failure).
        finished:
          type: integer
          description: Completed + stopped + cancelled.
        activeOrScheduled:
          type: integer
        errors:
          type: integer
          description: All failed-delivery outcomes combined — `error` + `bounced` + `unsubscribed`.
        responseRatePercent:
          type: number
