openapi: 3.1.0
info:
  title: Genaya API
  version: "1.0.0"
  description: |
    # Genaya API

    The Genaya REST API lets you build on top of your Genaya organization:
    read and write appointments, clients, contacts, invoices, estimates,
    expenses, lead tables and rows, tasks, task environments, reminders, and
    campaign audiences (push your signups straight into a marketing audience),
    plus read-only access to your team roster.

    The API is available on the **Business plan and above**. Everything is
    scoped to your organization by the API key you use - you can never see or
    touch another organization's data.

    Base URL:

    ```
    https://api.genaya.com/v1
    ```

    ## Getting started

    1. In the Genaya app, open **Settings > Developer** and create an API key.
       The full key is shown once at creation - store it securely.
    2. Send the key as a bearer token on every request:

    ```bash
    curl https://api.genaya.com/v1/appointments \
      -H "Authorization: Bearer gny_live_your_key_here"
    ```

    ## Authentication

    Authenticate with a Genaya-issued API key in the `Authorization` header:

    ```
    Authorization: Bearer gny_live_xxx
    ```

    Keys come in two modes:

    - `gny_live_...` - production traffic.
    - `gny_test_...` - operates on the **same** organization data but is flagged
      so you can separate test traffic. There is no isolated sandbox dataset.

    A missing, malformed, or revoked key returns `401 authentication_error`. A
    key whose organization is not on the Business plan returns `403
    permission_error` with code `plan_required`.

    API keys are server-to-server credentials. Do not embed them in browser or
    mobile apps.

    ## Scopes

    Each key carries optional per-resource scopes, for example
    `appointments:read` or `invoices:write`. A key with **no scopes** has full
    access (forward-compatible default). When scopes are present, a request that
    needs a scope the key lacks returns `403 permission_error` with code
    `insufficient_scope`. Each endpoint documents the scope it requires.

    ## Errors

    Errors use a consistent envelope:

    ```json
    {
      "error": {
        "type": "invalid_request_error",
        "code": "missing_field",
        "message": "clientName is required.",
        "param": "clientName"
      }
    }
    ```

    | HTTP | type | Meaning |
    |------|------|---------|
    | 400 | `invalid_request_error` | Malformed or missing parameters |
    | 401 | `authentication_error` | Bad, missing, or revoked key |
    | 403 | `permission_error` | Key lacks the scope, or org not on Business |
    | 404 | `not_found` | No such resource in this organization |
    | 409 | `idempotency_error` | Idempotency-Key reused with a different body |
    | 422 | `validation_error` | Well-formed but rejected by a business rule |
    | 429 | `rate_limit_error` | Rate limited (see Retry-After) |
    | 500 | `api_error` | Unexpected server error |

    ## Success envelope

    - Single object: `{ "data": { ... } }`
    - List: `{ "object": "list", "data": [ ... ], "has_more": true, "next_cursor": "<id>" }`

    Every resource object includes `id`, `object`, `createdAt`, and `updatedAt`
    (ISO-8601 strings). Fields are canonical camelCase.

    ## Pagination

    Lists are cursor-based. Pass `limit` (default 25, max 100) and
    `starting_after=<resourceId>` to page forward. The response includes
    `has_more` and `next_cursor`; keep requesting with
    `starting_after=next_cursor` until `has_more` is `false`. Lists are returned
    in a stable order; sort client-side on `createdAt` if you need chronological
    order.

    ## Filtering by created date

    Every list endpoint accepts optional `from` and `to` query parameters that
    filter by the resource's **created date**. Pass an ISO-8601 date
    (`2026-06-01`) or full timestamp (`2026-06-01T00:00:00Z`). A bare date for
    `from` snaps to the start of that UTC day and `to` to the end, so
    `?from=2026-06-01&to=2026-06-30` returns everything created in June. When a
    date window is applied, results are ordered newest-first by created date.
    (The team roster endpoint has no date window.)

    ## Idempotency

    Safely retry create requests by sending an `Idempotency-Key` header (any
    unique string). Genaya stores the first response for 24 hours and replays it
    on a retry with the same key. Reusing a key with a different request body
    returns `409 idempotency_error`.

    ## Rate limits

    Requests are rate limited per key. Every response carries
    `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. When
    you exceed a limit you get `429 rate_limit_error` with a `Retry-After`
    header (seconds).

    ## Versioning

    The API is versioned in the path (`/v1`). Breaking changes ship as a new
    version (`/v2`); `/v1` is a stable, frozen contract.

    ## Coming soon

    Outbound webhooks and official SDKs are not part of v1 yet.
  contact:
    name: Genaya Support
    url: https://help.genaya.com
    email: help@genaya.com
servers:
  - url: https://api.genaya.com/v1
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Appointments
    description: Schedule entries - appointments, projects, service calls, visits.
  - name: Clients
    description: People and companies you serve (clients, patients, customers).
  - name: Contacts
    description: Lightweight contacts used across messaging and call surfaces.
  - name: Lead Tables
    description: Lead table definitions (columns/schema). Read-only in v1.
  - name: Leads
    description: Lead rows inside a lead table.
  - name: Audiences
    description: Campaign audiences and their contacts - push signups into a marketing audience.
  - name: Estimates
    description: Quotes/estimates with line items, totals, and a send + convert flow.
  - name: Tasks
    description: Tasks inside task environments and groups.
  - name: Task Environments
    description: Containers (boards) that group tasks.
  - name: Reminders
    description: Personal reminders scoped to the key's issuing member.
  - name: Invoices
    description: Invoices with line items, totals, balances, and a send flow.
  - name: Expenses
    description: Recorded expenses with line items and totals.
  - name: Team
    description: The organization's team members (roster). Read-only in v1.
  - name: Messages
    description: Outbound SMS and MMS, plus the organization's message history.
  - name: Genaya AI
    description: Ask plain-language questions about your own data.
  - name: Calls
    description: Call history. Read-only.
  - name: Rooms
    description: Video rooms, bookable room events, and their bookings. Read-only.
  - name: Operations
    description: >-
      Read-only reference data - the configured options that write endpoints
      expect (appointment types, lead sources, tags), plus companies and phone
      numbers.
  - name: Account
    description: Who this API key is, what it may do, and how much it has used.
  - name: Engage
    description: Your public profile, reviews, forms and booking pages, and what they collect.
  - name: Activities
    description: The change history of a single record.
  - name: Payments
    description: Payment history, and recording payments taken outside Genaya.
  - name: PayDesk
    description: Payroll - commissions, runs and pay periods. Read-only.
x-tagGroups:
  - name: Core
    tags:
      - Appointments
      - Clients
      - Contacts
      - Lead Tables
      - Leads
  - name: Marketing
    tags:
      - Audiences
  - name: Sales
    tags:
      - Estimates
      - Invoices
      - Expenses
  - name: Work
    tags:
      - Tasks
      - Task Environments
      - Reminders
  - name: Communication
    tags:
      - Messages
      - Calls
      - Rooms
  - name: Intelligence
    tags:
      - Genaya AI
  - name: Engage
    tags:
      - Engage
  - name: Money
    tags:
      - Payments
      - PayDesk
  - name: Organization
    tags:
      - Operations
      - Team
      - Activities
      - Account
paths:
  /appointments:
    get:
      tags:
        - Appointments
      summary: List appointments
      description: >-
        Returns a cursor-paginated list of appointments for the authenticated
        organization. Soft-deleted appointments are excluded. Results are ordered
        by document id for stable, complete cursor coverage; each object carries
        its own createdAt for client-side sorting.
        Requires scope: appointments:read.
      operationId: listAppointments
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
      responses:
        '200':
          description: A paginated list of appointments.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Appointment'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags:
        - Appointments
      summary: Create an appointment
      description: >-
        Creates a new appointment for the authenticated organization. The
        organizationId and the creating actor are derived from the API key
        context and can never be set from the request body. Per-organization
        required-field configuration is enforced and returns 422 when a configured
        required field is missing. Supports the Idempotency-Key header for safe
        retries.
        Requires scope: appointments:write.
      operationId: createAppointment
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppointmentInput'
      responses:
        '201':
          description: The created appointment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Appointment'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /appointments/{id}:
    get:
      tags:
        - Appointments
      summary: Retrieve an appointment
      description: >-
        Returns a single appointment by id. A record belonging to another organization
        returns 404 rather than 403, so this can never confirm cross-tenant
        existence. Requires scope: appointments:read.
      operationId: getAppointment
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The appointment id.
      responses:
        '200':
          description: The requested appointment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Appointment'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    patch:
      tags:
        - Appointments
      summary: Update an appointment
      description: >-
        Updates an existing appointment. Ownership is verified before any write,
        so a cross-organization id returns 404. Only the fields supplied in the
        body are changed (merge semantics). Per-organization required-field
        configuration is enforced against the post-merge state, so a PATCH cannot
        blank a configured-required field.
        Requires scope: appointments:write.
      operationId: updateAppointment
      parameters:
        - name: id
          in: path
          required: true
          description: The id of the appointment to update.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppointmentInput'
      responses:
        '200':
          description: The updated appointment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Appointment'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    delete:
      tags:
        - Appointments
      summary: Delete an appointment
      description: >-
        Soft-deletes an appointment (it is marked deleted, never hard-removed).
        Ownership is verified before the write, so a cross-organization id returns
        404. Returns a deletion confirmation object.
        Requires scope: appointments:write.
      operationId: deleteAppointment
      parameters:
        - name: id
          in: path
          required: true
          description: The id of the appointment to delete.
          schema:
            type: string
      responses:
        '200':
          description: The deletion confirmation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/DeletedAppointment'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /clients:
    get:
      tags:
        - Clients
      summary: List clients
      description: >-
        Returns a cursor-paginated list of clients in your organization, ordered by
        document id. Soft-deleted clients are excluded. Requires scope: clients:read.
      operationId: listClients
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
      responses:
        '200':
          description: A paginated list of clients.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Client'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags:
        - Clients
      summary: Create a client
      description: >-
        Creates a client. Provide at least one of firstName, lastName, company,
        email, or phone. If the contact matches an existing active client (by email,
        then phone, then secondary phone) that client is updated and returned
        instead of creating a duplicate. Requires scope: clients:write.
      operationId: createClient
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClientCreate'
      responses:
        '201':
          description: The created (or upserted) client.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Client'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /clients/{id}:
    get:
      tags:
        - Clients
      summary: Retrieve a client
      description: >-
        Returns a single client by id. A record belonging to another organization
        returns 404 rather than 403, so this can never confirm cross-tenant
        existence. Requires scope: clients:read.
      operationId: getClient
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The client id.
      responses:
        '200':
          description: The requested client.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Client'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    patch:
      tags:
        - Clients
      summary: Update a client
      description: >-
        Updates an existing client. Ownership is verified before the write, so a
        client belonging to another organization returns 404. Requires scope:
        clients:write.
      operationId: updateClient
      parameters:
        - name: id
          in: path
          required: true
          description: The client id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClientUpdate'
      responses:
        '200':
          description: The updated client.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Client'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    delete:
      tags:
        - Clients
      summary: Delete a client
      description: >-
        Soft-deletes a client and revokes any client portal access tokens for that
        client. Requires scope: clients:write.
      operationId: deleteClient
      parameters:
        - name: id
          in: path
          required: true
          description: The client id.
          schema:
            type: string
      responses:
        '200':
          description: The client was soft-deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ClientDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /contacts:
    get:
      tags:
        - Contacts
      summary: List contacts
      description: >-
        Returns a cursor-paginated list of contacts. Contacts are a per-user
        private address book, so a key only ever sees the contacts created by its
        own issuing member, never the whole organization's. Soft-deleted contacts
        are excluded. Requires scope: contacts:read.
      operationId: listContacts
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
      responses:
        '200':
          description: A paginated list of contacts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Contact'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags:
        - Contacts
      summary: Create a contact
      description: >-
        Creates a new contact. At least one of name or phone must be provided.
        The address fullAddress field is always recomputed server-side from the
        address parts. Supports idempotent retries via the Idempotency-Key
        header. Requires scope: contacts:write.
      operationId: createContact
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactInput'
      responses:
        '201':
          description: The newly created contact.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Contact'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /contacts/{id}:
    get:
      tags:
        - Contacts
      summary: Retrieve a contact
      description: >-
        Returns a single contact by id. A record belonging to another organization
        returns 404 rather than 403, so this can never confirm cross-tenant
        existence. Requires scope: contacts:read.
      operationId: getContact
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The contact id.
      responses:
        '200':
          description: The requested contact.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Contact'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    patch:
      tags:
        - Contacts
      summary: Update a contact
      description: >-
        Partially updates a contact. Only the fields supplied in the request
        body are rewritten. The name-or-phone rule is enforced against the merged
        result, so a contact can never end up without both. Requires scope:
        contacts:write.
      operationId: updateContact
      parameters:
        - name: id
          in: path
          required: true
          description: The unique identifier of the contact.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactInput'
      responses:
        '200':
          description: The updated contact.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Contact'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    delete:
      tags:
        - Contacts
      summary: Delete a contact
      description: >-
        Soft-deletes a contact by setting its deletedAt timestamp. The contact is
        thereafter excluded from list and retrieve responses. Requires scope:
        contacts:write.
      operationId: deleteContact
      parameters:
        - name: id
          in: path
          required: true
          description: The unique identifier of the contact.
          schema:
            type: string
      responses:
        '200':
          description: Confirmation that the contact was deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/DeletedContact'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /lead-tables:
    get:
      tags:
        - Lead Tables
      summary: List lead tables
      description: >-
        Returns an org-wide, cursor-paginated list of lead tables (the table
        schema documents that hold the columns each lead row is stored against).
        Soft-deleted tables are excluded. An API key sees EVERY table in the org -
        the per-creator/assignee privacy lockdown applied inside the app does NOT
        apply to API keys. Requires scope: leads:read.
      operationId: listLeadTables
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
      responses:
        '200':
          description: A paginated list of lead tables.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/LeadTable'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags:
        - Lead Tables
      summary: Create a lead table
      description: >-
        Creates a lead table. Requires scope: leads:write.


        Every table is created with the same two frozen system columns the app
        creates (First Name, Last Name), so a table made through the API behaves
        identically to one made in the UI. Additional columns may be declared with
        `columns`; each gets a stable `nameId` derived from its name, which is the
        key lead rows store their cells under.


        Member assignment is not accepted here. An API key is an organization
        credential with no member identity, so tables it creates are visible
        org-wide - assign them in the app.
      operationId: createLeadTable
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LeadTableCreate'
      responses:
        '201':
          description: The created lead table.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/LeadTable'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /lead-tables/{id}:
    get:
      tags:
        - Lead Tables
      summary: Retrieve a lead table
      description: >-
        Returns a single lead table (its tabs and column schema) by id. Returns 404
        if the table is missing, soft-deleted, or belongs to another organization.
        Requires scope: leads:read.
      operationId: getLeadTable
      parameters:
        - name: id
          in: path
          required: true
          description: The id of the lead table.
          schema:
            type: string
      responses:
        '200':
          description: The requested lead table.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/LeadTable'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    delete:
      tags:
        - Lead Tables
      summary: Move a lead table to trash
      description: >-
        Moves a lead table to trash. Requires scope: leads:write.


        This is a soft delete. The lead rows inside are deliberately left intact -
        permanently destroying a table and every row under it is a confirmable,
        undoable-until-purged action that belongs in the app, not behind an API
        key.
      operationId: deleteLeadTable
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The table was moved to trash.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      object:
                        type: string
                        example: lead_table
                      deleted:
                        type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /lead-tables/{tableId}/leads:
    get:
      tags:
        - Leads
      summary: List leads in a table
      description: >-
        Returns a cursor-paginated list of lead rows belonging to the given lead
        table, ordered by document id. Soft-deleted rows are excluded. Reads
        Firestore directly so a just-created row appears immediately.
        Requires scope: leads:read.
      operationId: listLeads
      parameters:
        - name: tableId
          in: path
          required: true
          description: The id of the parent lead table.
          schema:
            type: string
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
      responses:
        '200':
          description: A paginated list of leads.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Lead'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags:
        - Leads
      summary: Create a lead
      description: >-
        Creates a new lead row in the given table. The row is fully populated in
        one call by writing the dynamic `fields` cell values verbatim. The target
        tab resolves from an explicit `tabId` (must match a tab on the table) or
        falls back to the table's first tab. The writer is the API key's issuing
        member. Requires scope: leads:write.
      operationId: createLead
      parameters:
        - name: tableId
          in: path
          required: true
          description: The id of the parent lead table.
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LeadCreate'
      responses:
        '201':
          description: The created lead.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Lead'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /lead-tables/{tableId}/leads/{leadId}:
    get:
      tags:
        - Leads
      summary: Retrieve a lead
      description: >-
        Returns a single lead row by id. The row must belong to both the caller's
        organization and the table in the path, so a valid row id under the wrong
        table returns 404. Requires scope: leads:read.
      operationId: getLead
      parameters:
        - name: tableId
          in: path
          required: true
          schema:
            type: string
        - name: leadId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested lead.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Lead'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    patch:
      tags:
        - Leads
      summary: Update a lead
      description: >-
        Updates a lead row's dynamic cells and/or its `tabId`, `starred`, and
        `displayOrder`. The `fields` map is merged onto the row verbatim (raw
        values, no type coercion). Reserved keys can never be written through
        `fields`. A supplied non-empty `tabId` must match a tab on the table.
        Requires scope: leads:write.
      operationId: updateLead
      parameters:
        - name: tableId
          in: path
          required: true
          description: The id of the parent lead table.
          schema:
            type: string
        - name: leadId
          in: path
          required: true
          description: The id of the lead row.
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LeadUpdate'
      responses:
        '200':
          description: The updated lead.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Lead'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    delete:
      tags:
        - Leads
      summary: Delete a lead
      description: >-
        Soft-deletes a lead row (sets `deletedAt` so it drops out of every read
        surface while staying recoverable). Returns 404 if the row is missing,
        cross-organization, belongs to a different table, or already soft-deleted.
        Requires scope: leads:write.
      operationId: deleteLead
      parameters:
        - name: tableId
          in: path
          required: true
          description: The id of the parent lead table.
          schema:
            type: string
        - name: leadId
          in: path
          required: true
          description: The id of the lead row.
          schema:
            type: string
      responses:
        '200':
          description: The lead was soft-deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/DeletedLead'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /audiences:
    get:
      tags:
        - Audiences
      summary: List audiences
      description: >-
        Returns a cursor-paginated list of the organization's campaign audiences
        (metadata only - the recipient contacts are paginated separately via the
        contacts sub-resource). Soft-deleted audiences are excluded. Requires
        scope: audiences:read.
      operationId: listAudiences
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
      responses:
        '200':
          description: A paginated list of audiences.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Audience'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags:
        - Audiences
      summary: Create an audience
      description: >-
        Provisions a new, empty audience so you can push contacts into it from your
        own systems. The audience is created with source "api" and zero recipients;
        add contacts with the contacts sub-resource. Supports idempotent retries
        via the Idempotency-Key header. Requires scope: audiences:write.
      operationId: createAudience
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AudienceCreate'
      responses:
        '201':
          description: The newly created audience.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Audience'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /audiences/{audienceId}:
    get:
      tags:
        - Audiences
      summary: Retrieve an audience
      description: >-
        Returns a single audience by id (metadata only). Returns 404 if the
        audience is missing, soft-deleted, or belongs to another organization.
        Requires scope: audiences:read.
      operationId: getAudience
      parameters:
        - name: audienceId
          in: path
          required: true
          description: The id of the audience.
          schema:
            type: string
      responses:
        '200':
          description: The requested audience.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Audience'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /audiences/{audienceId}/contacts:
    get:
      tags:
        - Audiences
      summary: List audience contacts
      description: >-
        Returns a cursor-paginated list of the contacts in an audience's send pool,
        ordered by email. Each contact carries a `suppressed` flag reflecting the
        organization's suppression list - a suppressed address stays listed but is
        skipped at send. The cursor (`starting_after`) is a contact's email.
        Requires scope: audiences:read.
      operationId: listAudienceContacts
      parameters:
        - name: audienceId
          in: path
          required: true
          description: The id of the audience.
          schema:
            type: string
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
      responses:
        '200':
          description: A paginated list of audience contacts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AudienceContact'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags:
        - Audiences
      summary: Add a contact to an audience
      description: >-
        Adds or upserts a contact into the audience's send pool. Fire this on each
        new signup to auto-sync your audience: new emails are added, and pushing an
        email that already exists updates its name fields instead of duplicating
        (the response `created` flag tells you which happened). Email match is
        case-insensitive. A suppressed/unsubscribed address is NOT added to the
        send pool - the response returns success with `suppressed: true` so you
        know it will not receive mail. Supports idempotent retries via the
        Idempotency-Key header. Requires scope: audiences:write.
      operationId: addAudienceContact
      parameters:
        - name: audienceId
          in: path
          required: true
          description: The id of the audience.
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AudienceContactInput'
      responses:
        '200':
          description: The contact was added, updated, or flagged as suppressed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AudienceContactResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /audiences/{audienceId}/contacts/{email}:
    delete:
      tags:
        - Audiences
      summary: Remove a contact from an audience
      description: >-
        Removes a contact from the audience's send pool by email. Idempotent:
        removing an email that is not in the audience returns a clean result with
        `removed: false` (never a 404). Updates the audience count. Requires scope:
        audiences:write.
      operationId: deleteAudienceContact
      parameters:
        - name: audienceId
          in: path
          required: true
          description: The id of the audience.
          schema:
            type: string
        - name: email
          in: path
          required: true
          description: The email address of the contact to remove (case-insensitive).
          schema:
            type: string
      responses:
        '200':
          description: Confirmation that the contact was removed (or was already absent).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/DeletedAudienceContact'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /estimates:
    get:
      tags:
        - Estimates
      summary: List estimates
      description: >-
        Returns a cursor-paginated list of estimates for the authenticated
        organization, ordered by document id. Soft-deleted estimates are excluded.
        Requires scope: estimates:read.
      operationId: listEstimates
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
      responses:
        '200':
          description: A paginated list of estimates.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Estimate'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags:
        - Estimates
      summary: Create an estimate
      description: >-
        Creates a new estimate. The organization and the actor (CreatedBy) are
        always taken from the API key context, never the request body. Per-line
        amounts and the subtotal/taxTotal/total are computed server-side from the
        supplied line items and are not trusted from the body. Requires scope:
        estimates:write.
      operationId: createEstimate
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EstimateCreate'
      responses:
        '201':
          description: The created estimate.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Estimate'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /estimates/{id}:
    get:
      tags:
        - Estimates
      summary: Retrieve an estimate
      description: >-
        Returns a single estimate by id. A record belonging to another organization
        returns 404 rather than 403, so this can never confirm cross-tenant
        existence. Requires scope: estimates:read.
      operationId: getEstimate
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The estimate id.
      responses:
        '200':
          description: The requested estimate.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Estimate'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    patch:
      tags:
        - Estimates
      summary: Update an estimate
      description: >-
        Updates an existing estimate. Only the supplied fields are changed. When
        lineItems are supplied, the subtotal/taxTotal/total are recomputed
        server-side and never trusted from the body. Setting status to "sent"
        stamps sentAt; setting it to "draft" clears the sent flag. Requires scope:
        estimates:write.
      operationId: updateEstimate
      parameters:
        - name: id
          in: path
          required: true
          description: The estimate document id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EstimateUpdate'
      responses:
        '200':
          description: The updated estimate.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Estimate'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    delete:
      tags:
        - Estimates
      summary: Delete an estimate
      description: >-
        Soft-deletes an estimate (sets softDeleted, deletedAt). There is no hard
        delete for estimates. Returns a deletion confirmation object. Requires
        scope: estimates:write.
      operationId: deleteEstimate
      parameters:
        - name: id
          in: path
          required: true
          description: The estimate document id.
          schema:
            type: string
      responses:
        '200':
          description: Deletion confirmation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/EstimateDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /estimates/{id}/send:
    post:
      tags:
        - Estimates
      summary: Mark an estimate as sent
      description: >-
        Records the estimate as sent (sets sent, stamps sentAt; transitions a
        draft to "sent"). Already-resolved statuses (accepted, declined, expired,
        void) keep their status. This does NOT email the estimate - the caller
        drives their own delivery. Requires scope: estimates:write.
      operationId: sendEstimate
      parameters:
        - name: id
          in: path
          required: true
          description: The estimate document id.
          schema:
            type: string
      responses:
        '200':
          description: The estimate after being marked as sent.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Estimate'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /estimates/{id}/convert-to-invoice:
    post:
      tags:
        - Estimates
      summary: Convert an estimate to an invoice
      description: >-
        Creates a new invoice from the estimate, copying its line items, client
        fields, and billed-from fields. The new invoice starts as status "draft"
        with balance equal to its total and amountPaid 0. The estimate is stamped
        with the resulting invoice id. Guards against double-conversion: if the
        estimate was already converted, returns 422. Requires both scopes:
        estimates:write and invoices:write.
      operationId: convertEstimateToInvoice
      parameters:
        - name: id
          in: path
          required: true
          description: The estimate document id.
          schema:
            type: string
      responses:
        '201':
          description: The newly created invoice.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/EstimateInvoice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /tasks:
    get:
      tags:
        - Tasks
      summary: List tasks
      description: >-
        Returns a cursor-paginated list of tasks for the authenticated
        organization, ordered by document id. Soft-deleted tasks are excluded.
        Supports optional equality filters environmentId and groupId. Tasks have
        no leadId field, so no lead filter is available. Requires scope:
        tasks:read.
      operationId: listTasks
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
        - name: environmentId
          in: query
          required: false
          description: Filter to tasks belonging to this environment (exact match).
          schema:
            type: string
        - name: groupId
          in: query
          required: false
          description: Filter to tasks belonging to this group (exact match).
          schema:
            type: string
      responses:
        '200':
          description: A paginated list of tasks.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Task'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags:
        - Tasks
      summary: Create a task
      description: >-
        Creates a task. title and groupId are required. environmentId is DERIVED
        from the group (it cannot be set directly); the group must be live and
        belong to the organization or a 422 is returned. subGroupId is honored
        only when it exactly matches one of the group's subGroups, otherwise the
        task is left ungrouped (null). priority defaults to normal when omitted or
        invalid. position is auto-computed as max(existing in group)+1 when
        omitted. The fields map is stored raw with no validation. Send an
        Idempotency-Key header to safely retry. Requires scope: tasks:write.
      operationId: createTask
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TaskCreate'
      responses:
        '201':
          description: The created task.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Task'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /tasks/{id}:
    get:
      tags:
        - Tasks
      summary: Retrieve a task
      description: >-
        Returns a single task by id. A record belonging to another organization
        returns 404 rather than 403, so this can never confirm cross-tenant
        existence. Requires scope: tasks:read.
      operationId: getTask
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The task id.
      responses:
        '200':
          description: The requested task.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Task'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    patch:
      tags:
        - Tasks
      summary: Update a task
      description: >-
        Partially updates a task. Only supplied fields are changed. title cannot
        be set to empty. Changing groupId re-derives environmentId from the new
        (live, same-org) group and clears any now-stale subGroupId unless a new
        subGroupId is supplied. subGroupId is validated against the effective
        group; an empty or unknown value clears it. priority falls back to normal
        when invalid. The fields map replaces customFields raw. Requires scope:
        tasks:write.
      operationId: updateTask
      parameters:
        - name: id
          in: path
          required: true
          description: The task id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TaskUpdate'
      responses:
        '200':
          description: The updated task.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Task'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    delete:
      tags:
        - Tasks
      summary: Delete a task
      description: >-
        Soft-deletes a task (sets deletedAt). The task is never hard-deleted and
        will no longer appear in lists or single retrievals. Requires scope:
        tasks:write.
      operationId: deleteTask
      parameters:
        - name: id
          in: path
          required: true
          description: The task id.
          schema:
            type: string
      responses:
        '200':
          description: The soft-delete confirmation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TaskDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /task-environments:
    get:
      tags:
        - Task Environments
      summary: List task environments
      description: >-
        Returns an org-wide list of every active task environment (the top level
        of the work-management board). No auto-seed and no per-user filtering -
        every active environment in the organization is returned. Requires scope:
        tasks:read.
      operationId: listTaskEnvironments
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
      responses:
        '200':
          description: A paginated list of task environments.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskEnvironment'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags:
        - Task Environments
      summary: Create a task environment
      description: >-
        Creates a single task environment document. Unlike the internal UI
        endpoint, no default groups are batch-seeded - exactly the one resource
        requested is created. The organization and actor are derived from the API
        key context, never the request body. When position is omitted it is
        server-derived as max(existing position)+1 within the org's active
        environments. Requires scope: tasks:write.
      operationId: createTaskEnvironment
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TaskEnvironmentCreate'
      responses:
        '201':
          description: The created task environment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TaskEnvironment'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /task-environments/{id}:
    get:
      tags:
        - Task Environments
      summary: Retrieve a task environment
      description: >-
        Returns a single task environment by id. A record belonging to another organization
        returns 404 rather than 403, so this can never confirm cross-tenant
        existence. Requires scope: tasks:read.
      operationId: getTaskEnvironment
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The task environment id.
      responses:
        '200':
          description: The requested task environment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TaskEnvironment'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    patch:
      tags:
        - Task Environments
      summary: Update a task environment
      description: >-
        Updates only the provided fields of a task environment. Cross-org or
        soft-deleted resources return 404. Requires scope: tasks:write.
      operationId: updateTaskEnvironment
      parameters:
        - name: id
          in: path
          required: true
          description: The task environment ID.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TaskEnvironmentUpdate'
      responses:
        '200':
          description: The updated task environment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TaskEnvironment'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    delete:
      tags:
        - Task Environments
      summary: Delete a task environment
      description: >-
        Soft-deletes a task environment and cascades the soft-delete to every
        active group and task within it, in a single batch. Org-scoped
        throughout. Requires scope: tasks:write.
      operationId: deleteTaskEnvironment
      parameters:
        - name: id
          in: path
          required: true
          description: The task environment ID.
          schema:
            type: string
      responses:
        '200':
          description: The soft-delete confirmation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TaskEnvironmentDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /task-environments/{id}/groups:
    get:
      tags:
        - Task Environments
      summary: List a task environment's groups
      description: >-
        Lists the task groups that belong to a single environment, org-scoped and
        soft-delete filtered, position-ordered. Use a returned group id as the
        groupId required to create a task (the tasks API takes a groupId, not an
        environmentId). A cross-org or soft-deleted environment returns 404.
        Requires scope: tasks:read.
      operationId: listTaskEnvironmentGroups
      parameters:
        - name: id
          in: path
          required: true
          description: The task environment ID.
          schema:
            type: string
      responses:
        '200':
          description: The environment's task groups.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskGroup'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags:
        - Task Environments
      summary: Create a task group
      description: >-
        Creates a task group inside an environment. A group is the container a task
        is filed under, so this is how you build a board from scratch and then
        create tasks against it (POST /tasks requires a groupId). position
        auto-derives to the end of the environment when omitted. A cross-org or
        soft-deleted environment returns 404. Supports idempotent retries via the
        Idempotency-Key header. Requires scope: tasks:write.
      operationId: createTaskGroup
      parameters:
        - name: id
          in: path
          required: true
          description: The task environment ID.
          schema:
            type: string
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TaskGroupCreate'
      responses:
        '201':
          description: The newly created task group.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TaskGroup'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /reminders:
    get:
      tags:
        - Reminders
      summary: List reminders
      description: >-
        Returns a cursor-paginated list of the reminders owned by the API key's
        issuing member, scoped to the key's organization. A key only ever sees the
        reminders created by its own member, never the whole organization's.
        Requires scope: reminders:read.
      operationId: listReminders
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
      responses:
        '200':
          description: A paginated list of reminders.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Reminder'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags:
        - Reminders
      summary: Create a reminder
      description: >-
        Creates a reminder owned by the API key's issuing member. The status is
        server-controlled and always starts as inProgress; resolvedAt is
        server-computed. The organization and owner are taken from the key
        context and never from the request body. Supports the Idempotency-Key
        header for safe retries. Requires scope: reminders:write.
      operationId: createReminder
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReminderCreate'
      responses:
        '201':
          description: The newly created reminder.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Reminder'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /reminders/{id}:
    get:
      tags:
        - Reminders
      summary: Retrieve a reminder
      description: >-
        Returns a single reminder by id, scoped to the key's organization and its
        issuing member. Returns 404 if the reminder is missing, soft-deleted,
        belongs to another organization, or was created by another member.
        Requires scope: reminders:read.
      operationId: getReminder
      parameters:
        - name: id
          in: path
          required: true
          description: The id of the reminder.
          schema:
            type: string
      responses:
        '200':
          description: The requested reminder.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Reminder'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    patch:
      tags:
        - Reminders
      summary: Update a reminder
      description: >-
        Updates a reminder. Each field is applied only when present in the body.
        Setting remindAt reschedules the reminder and re-arms its notification.
        Setting status to resolved stamps resolvedAt; setting it back to
        inProgress clears resolvedAt. The reminder must belong to the key's
        organization and be owned by the key's issuing member. Requires scope:
        reminders:write.
      operationId: updateReminder
      parameters:
        - name: id
          in: path
          required: true
          description: The reminder identifier.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReminderUpdate'
      responses:
        '200':
          description: The updated reminder.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Reminder'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    delete:
      tags:
        - Reminders
      summary: Delete a reminder
      description: >-
        Soft-deletes a reminder. The reminder must belong to the key's
        organization and be owned by the key's issuing member. Requires scope:
        reminders:write.
      operationId: deleteReminder
      parameters:
        - name: id
          in: path
          required: true
          description: The reminder identifier.
          schema:
            type: string
      responses:
        '200':
          description: The deletion confirmation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/DeletedReminder'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /invoices:
    get:
      tags:
        - Invoices
      summary: List invoices
      description: >-
        Returns a cursor-paginated list of invoices for the organization that owns
        the API key. Results are ordered by document id and exclude soft-deleted
        invoices. Requires scope: invoices:read.
      operationId: listInvoices
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
      responses:
        '200':
          description: A paginated list of invoices.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Invoice'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags:
        - Invoices
      summary: Create an invoice
      description: >-
        Creates a new invoice. The provided lineItems are validated and the
        subtotal, tax total, total, and balance are computed server-side - any
        caller-sent totals are ignored. A sequential business number is assigned
        from the organization's numbering settings. At least one line item is
        required. Requires scope: invoices:write.
      operationId: createInvoice
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoiceCreate'
      responses:
        '201':
          description: The created invoice.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Invoice'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /invoices/{id}:
    get:
      tags:
        - Invoices
      summary: Retrieve an invoice
      description: >-
        Returns a single invoice by id. A record belonging to another organization
        returns 404 rather than 403, so this can never confirm cross-tenant
        existence. Requires scope: invoices:read.
      operationId: getInvoice
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The invoice id.
      responses:
        '200':
          description: The requested invoice.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Invoice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    patch:
      tags:
        - Invoices
      summary: Update an invoice
      description: >-
        Updates an existing invoice. Only the supplied fields are changed. When
        lineItems are provided, the subtotal, tax total, total, and balance are
        recomputed server-side - caller-sent totals are never trusted. Line-item
        edits are rejected with a 422 once the invoice is paid, completed,
        fulfilled, cancelled, or voided. At least one line item is required when
        lineItems is supplied. Requires scope: invoices:write.
      operationId: updateInvoice
      parameters:
        - name: id
          in: path
          required: true
          description: The invoice id.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoiceUpdate'
      responses:
        '200':
          description: The updated invoice.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Invoice'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    delete:
      tags:
        - Invoices
      summary: Delete an invoice
      description: >-
        Soft-deletes an invoice. The invoice is never hard-deleted; it is stamped
        with a deletion timestamp and excluded from future reads. Requires scope:
        invoices:write.
      operationId: deleteInvoice
      parameters:
        - name: id
          in: path
          required: true
          description: The invoice id.
          schema:
            type: string
      responses:
        '200':
          description: The deletion confirmation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/InvoiceDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /invoices/{id}/send:
    post:
      tags:
        - Invoices
      summary: Send an invoice
      description: >-
        Marks an invoice as sent (sets the sent flag and a sent timestamp). If the
        invoice is currently a draft or has no status, its status transitions to
        "sent"; an invoice already past draft keeps its status. This endpoint does
        not email the client. Requires scope: invoices:write.
      operationId: sendInvoice
      parameters:
        - name: id
          in: path
          required: true
          description: The invoice id.
          schema:
            type: string
      responses:
        '200':
          description: The invoice after the send transition.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Invoice'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /expenses:
    get:
      tags:
        - Expenses
      summary: List expenses
      description: |
        Returns a cursor-paginated list of expenses for the authenticated organization.
        Soft-deleted expenses are excluded. Results are ordered by document id for stable,
        complete cursor coverage; each object still carries createdAt for client-side sorting.
        Requires scope: expenses:read.
      operationId: listExpenses
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
      responses:
        '200':
          description: A paginated list of expenses.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Expense'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags:
        - Expenses
      summary: Create an expense
      description: |
        Creates a new expense for the authenticated organization. You must provide a
        vendorName or at least one item; otherwise the request is rejected. The expense
        status is always set to "created" and the actor (createdBy) is the API key's
        issuing member, never a request body field. Supports idempotent retries via the
        Idempotency-Key header. Requires scope: expenses:write.
      operationId: createExpense
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExpenseCreate'
      responses:
        '201':
          description: The newly created expense.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Expense'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /expenses/{id}:
    get:
      tags:
        - Expenses
      summary: Retrieve an expense
      description: >-
        Returns a single expense by id. A record belonging to another organization
        returns 404 rather than 403, so this can never confirm cross-tenant
        existence. Requires scope: expenses:read.
      operationId: getExpense
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The expense id.
      responses:
        '200':
          description: The requested expense.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Expense'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    patch:
      tags:
        - Expenses
      summary: Update an expense
      description: |
        Updates an existing expense. Only the fields present in the request body are
        changed. The post-merge state must still have a vendorName or at least one item,
        otherwise the request is rejected. Returns 404 if the expense does not exist,
        belongs to another organization, or has been soft-deleted. Requires scope: expenses:write.
      operationId: updateExpense
      parameters:
        - name: id
          in: path
          required: true
          description: The id of the expense to update.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExpenseUpdate'
      responses:
        '200':
          description: The updated expense.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Expense'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    delete:
      tags:
        - Expenses
      summary: Delete an expense
      description: |
        Soft-deletes an expense. The record is retained internally but no longer appears
        in list or retrieve responses. Returns 404 if the expense does not exist, belongs
        to another organization, or was already soft-deleted. Requires scope: expenses:write.
      operationId: deleteExpense
      parameters:
        - name: id
          in: path
          required: true
          description: The id of the expense to delete.
          schema:
            type: string
      responses:
        '200':
          description: The delete confirmation.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/DeletedExpense'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /messages:
    get:
      tags:
        - Messages
      summary: List messages
      description: >-
        Returns a cursor-paginated list of the organization's SMS and MMS message
        history, newest ids last. Requires scope: messages:read.
      operationId: listMessages
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - name: direction
          in: query
          required: false
          schema:
            type: string
            enum:
              - inbound
              - outbound
          description: Return only messages sent or only messages received.
        - name: conversationId
          in: query
          required: false
          schema:
            type: string
          description: Return only messages in one conversation thread.
      responses:
        '200':
          description: A paginated list of messages.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Message'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags:
        - Messages
      summary: Send a message
      description: >-
        Sends an SMS or MMS from one of the organization's numbers. Requires
        scope: messages:write.


        Billing is per carrier segment, so a long body costs more than a short
        one. The organization's prepaid usage balance is checked before anything
        is sent - an organization out of credit gets 403 insufficient_balance and
        no message leaves.


        Strongly consider sending an Idempotency-Key. It matters more here than
        anywhere else in the API: a retried request without one can send the same
        text twice, which costs money and annoys the recipient.
      operationId: sendMessage
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessageCreate'
      responses:
        '201':
          description: The message was accepted by the carrier.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Message'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /messages/{id}:
    get:
      tags:
        - Messages
      summary: Retrieve a message
      description: >-
        Returns a single message by id. A message belonging to another
        organization returns 404 rather than 403. Requires scope: messages:read.
      operationId: getMessage
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested message.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Message'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /calls:
    get:
      tags:
        - Calls
      summary: List calls
      description: >-
        Returns the organization's call history. Carrier and provider identifiers are not exposed - they are operational internals with no integration use. Read-only. Requires scope: calls:read.
      operationId: listCalls
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - name: direction
          in: query
          required: false
          schema:
            type: string
            enum:
              - inbound
              - outbound
          description: Return only calls matching this direction.
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: Return only calls matching this status.
        - name: companyId
          in: query
          required: false
          schema:
            type: string
          description: Return only calls matching this companyId.
      responses:
        '200':
          description: A paginated list of calls.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Call'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /calls/{id}:
    get:
      tags:
        - Calls
      summary: Retrieve a call
      description: >-
        Returns a single call by id. A record belonging to another organization
        returns 404 rather than 403. Requires scope: calls:read.
      operationId: getCall
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested call.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Call'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /rooms:
    get:
      tags:
        - Rooms
      summary: List rooms
      description: >-
        Returns the organization's video rooms. Read-only. Requires scope: rooms:read.
      operationId: listRooms
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: Return only rooms matching this status.
      responses:
        '200':
          description: A paginated list of rooms.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Room'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /rooms/{id}:
    get:
      tags:
        - Rooms
      summary: Retrieve a room
      description: >-
        Returns a single room by id. A record belonging to another organization
        returns 404 rather than 403. Requires scope: rooms:read.
      operationId: getRoom
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested room.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Room'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /room-events:
    get:
      tags:
        - Rooms
      summary: List room events
      description: >-
        Returns bookable room event types - the meeting types invitees can schedule. Read-only. Requires scope: rooms:read.
      operationId: listRoomEvents
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - name: type
          in: query
          required: false
          schema:
            type: string
          description: Return only room events matching this type.
      responses:
        '200':
          description: A paginated list of room events.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/RoomEvent'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /room-events/{id}:
    get:
      tags:
        - Rooms
      summary: Retrieve a room event
      description: >-
        Returns a single room event by id. A record belonging to another organization
        returns 404 rather than 403. Requires scope: rooms:read.
      operationId: getRoomEvent
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested room event.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/RoomEvent'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /room-bookings:
    get:
      tags:
        - Rooms
      summary: List room bookings
      description: >-
        Returns scheduled room bookings, including who booked and when. Read-only. Requires scope: rooms:read.
      operationId: listRoomBookings
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - name: roomId
          in: query
          required: false
          schema:
            type: string
          description: Return only room bookings matching this roomId.
      responses:
        '200':
          description: A paginated list of room bookings.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/RoomBooking'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /room-bookings/{id}:
    get:
      tags:
        - Rooms
      summary: Retrieve a room booking
      description: >-
        Returns a single room booking by id. A record belonging to another organization
        returns 404 rather than 403. Requires scope: rooms:read.
      operationId: getRoomBooking
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested room booking.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/RoomBooking'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /ai/ask:
    post:
      tags:
        - Genaya AI
      summary: Ask Genaya AI about your data
      description: >-
        Asks a plain-language question about the organization's own records and
        returns a written answer. Requires scope: ai:read.


        The model never writes queries directly. It calls a fixed set of read-only
        tools and Genaya executes them, always scoped to the organization behind
        the API key - so a question can never reach another tenant's data.


        What it can see is derived from the key's own read scopes, and is limited
        to record types the key could already read through the REST endpoints.
        Asking the AI can never surface data that a direct GET with the same key
        would refuse. A key with no read scopes at all is rejected.


        Answers are billed as AI usage against the organization's plan budget.
        Model tiers are plan-gated; omit `tier` to let Genaya route the question
        to an appropriate one.
      operationId: askGenayaAi
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AiAsk'
      responses:
        '200':
          description: The answer, and what the model consulted to produce it.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AiAnswer'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /engage-profiles:
    get:
      tags:
        - Engage
      summary: List engage profiles
      description: >-
        Returns the organization's public Engage profile. Read-only. Requires scope: engage:read.
      operationId: listEngageProfiles
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
      responses:
        '200':
          description: A paginated list of engage profiles.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/EngageProfile'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /engage-profiles/{id}:
    get:
      tags:
        - Engage
      summary: Retrieve a engage profile
      description: >-
        Returns a single engage profile by id. A record belonging to another organization
        returns 404 rather than 403. Requires scope: engage:read.
      operationId: getEngageProfile
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested engage profile.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/EngageProfile'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /engage-reviews:
    get:
      tags:
        - Engage
      summary: List reviews
      description: >-
        Returns customer reviews. The reviewer's email, IP address and geolocation are never exposed. Read-only. Requires scope: engage:read.
      operationId: listEngageReviews
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - name: publicVisible
          in: query
          required: false
          schema:
            type: string
          description: Return only reviews matching this publicVisible.
      responses:
        '200':
          description: A paginated list of reviews.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/EngageReview'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /engage-reviews/{id}:
    get:
      tags:
        - Engage
      summary: Retrieve a review
      description: >-
        Returns a single review by id. A record belonging to another organization
        returns 404 rather than 403. Requires scope: engage:read.
      operationId: getEngageReview
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested review.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/EngageReview'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /engage-forms:
    get:
      tags:
        - Engage
      summary: List forms
      description: >-
        Returns published Engage forms. Read-only. Requires scope: engage:read.
      operationId: listEngageForms
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
      responses:
        '200':
          description: A paginated list of forms.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/EngageForm'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /engage-forms/{id}:
    get:
      tags:
        - Engage
      summary: Retrieve a form
      description: >-
        Returns a single form by id. A record belonging to another organization
        returns 404 rather than 403. Requires scope: engage:read.
      operationId: getEngageForm
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested form.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/EngageForm'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /engage-form-submissions:
    get:
      tags:
        - Engage
      summary: List form submissions
      description: >-
        Returns answers submitted through Engage forms. Read-only. Requires scope: engage:read.
      operationId: listEngageFormSubmissions
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - name: formId
          in: query
          required: false
          schema:
            type: string
          description: Return only form submissions matching this formId.
      responses:
        '200':
          description: A paginated list of form submissions.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/EngageFormSubmission'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /engage-form-submissions/{id}:
    get:
      tags:
        - Engage
      summary: Retrieve a form submission
      description: >-
        Returns a single form submission by id. A record belonging to another organization
        returns 404 rather than 403. Requires scope: engage:read.
      operationId: getEngageFormSubmission
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested form submission.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/EngageFormSubmission'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /engage-bookings:
    get:
      tags:
        - Engage
      summary: List booking pages
      description: >-
        Returns published Engage booking pages. Read-only. Requires scope: engage:read.
      operationId: listEngageBookings
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - name: companyId
          in: query
          required: false
          schema:
            type: string
          description: Return only booking pages matching this companyId.
      responses:
        '200':
          description: A paginated list of booking pages.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/EngageBooking'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /engage-bookings/{id}:
    get:
      tags:
        - Engage
      summary: Retrieve a booking page
      description: >-
        Returns a single booking page by id. A record belonging to another organization
        returns 404 rather than 403. Requires scope: engage:read.
      operationId: getEngageBooking
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested booking page.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/EngageBooking'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /engage-booking-submissions:
    get:
      tags:
        - Engage
      summary: List booking submissions
      description: >-
        Returns bookings made through Engage booking pages. Read-only. Requires scope: engage:read.
      operationId: listEngageBookingSubmissions
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - name: bookingId
          in: query
          required: false
          schema:
            type: string
          description: Return only booking submissions matching this bookingId.
        - name: companyId
          in: query
          required: false
          schema:
            type: string
          description: Return only booking submissions matching this companyId.
      responses:
        '200':
          description: A paginated list of booking submissions.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/EngageBookingSubmission'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /engage-booking-submissions/{id}:
    get:
      tags:
        - Engage
      summary: Retrieve a booking submission
      description: >-
        Returns a single booking submission by id. A record belonging to another organization
        returns 404 rather than 403. Requires scope: engage:read.
      operationId: getEngageBookingSubmission
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested booking submission.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/EngageBookingSubmission'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /payments:
    get:
      tags:
        - Payments
      summary: List payments
      description: >-
        Returns the organization's payment history. Stripe identifiers are never
        exposed. Requires scope: payments:read.
      operationId: listPayments
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - name: invoiceId
          in: query
          required: false
          schema:
            type: string
        - name: method
          in: query
          required: false
          schema:
            type: string
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - succeeded
              - failed
              - pending
        - name: provider
          in: query
          required: false
          schema:
            type: string
            enum:
              - genaya_pay
              - manual
      responses:
        '200':
          description: A paginated list of payments.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Payment'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags:
        - Payments
      summary: Record an offline payment
      description: >-
        Records a payment that was taken OUTSIDE Genaya - cash on site, a mailed
        check, a bank transfer - against an invoice, and recalculates that
        invoice's paid amount and balance. Requires scope: payments:write.


        This endpoint cannot charge a card, and that is deliberate. An API key is
        an organization-wide bearer credential with no second factor and no
        cardholder present. If it could charge a stored card, a single leaked key
        would be enough to run charges against your customers. Card payments stay
        on the paths that have a human or a cardholder in the loop: the app, the
        client portal, and Genaya payment links. Passing a card method here is
        rejected with `card_payments_not_supported`.


        Send an Idempotency-Key. Without one, a retry after a network timeout
        records the payment twice and the invoice shows as overpaid.
      operationId: recordOfflinePayment
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OfflinePaymentCreate'
      responses:
        '201':
          description: The recorded payment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Payment'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /payments/{id}:
    get:
      tags:
        - Payments
      summary: Retrieve a payment
      description: >-
        Returns a single payment by id. A payment belonging to another
        organization returns 404 rather than 403. Requires scope: payments:read.
      operationId: getPayment
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested payment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Payment'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /activities:
    get:
      tags:
        - Activities
      summary: List a record's activity timeline
      description: >-
        Returns the change history for ONE record. Requires scope:
        activities:read.


        Both `entityType` and `entityId` are required. Activity is recorded per
        record rather than per organization, so there is no organization-wide
        timeline to return; the record you name is authorized first, and a record
        belonging to another organization returns 404.
      operationId: listActivities
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - name: entityType
          in: query
          required: true
          schema:
            type: string
            enum:
              - appointment
              - client
              - invoice
              - estimate
              - expense
              - lead
        - name: entityId
          in: query
          required: true
          schema:
            type: string
          description: The id of the record whose timeline you want.
      responses:
        '200':
          description: A paginated list of activity entries.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Activity'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /paydesk-commissions:
    get:
      tags:
        - PayDesk
      summary: List commissions
      description: >-
        Returns commission records, including which rule produced each amount. Read-only. Requires scope: paydesk:read.
      operationId: listCommissions
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - name: employeeId
          in: query
          required: false
          schema:
            type: string
          description: Return only commissions matching this employeeId.
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - pending
              - eligible
              - locked
              - paid
              - reversed
          description: Return only commissions matching this status.
        - name: appointmentId
          in: query
          required: false
          schema:
            type: string
          description: Return only commissions matching this appointmentId.
      responses:
        '200':
          description: A paginated list of commissions.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Commission'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /paydesk-commissions/{id}:
    get:
      tags:
        - PayDesk
      summary: Retrieve a commission
      description: >-
        Returns a single commission by id. A record belonging to another organization
        returns 404 rather than 403. Requires scope: paydesk:read.
      operationId: getCommission
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested commission.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Commission'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /paydesk-payroll-runs:
    get:
      tags:
        - PayDesk
      summary: List payroll runs
      description: >-
        Returns payroll runs with their range totals. Per-employee breakdowns are not exposed here. Read-only. Requires scope: paydesk:read.
      operationId: listPayrollRuns
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: Return only payroll runs matching this status.
        - name: frequency
          in: query
          required: false
          schema:
            type: string
          description: Return only payroll runs matching this frequency.
      responses:
        '200':
          description: A paginated list of payroll runs.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PayrollRun'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /paydesk-payroll-runs/{id}:
    get:
      tags:
        - PayDesk
      summary: Retrieve a payroll run
      description: >-
        Returns a single payroll run by id. A record belonging to another organization
        returns 404 rather than 403. Requires scope: paydesk:read.
      operationId: getPayrollRun
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested payroll run.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PayrollRun'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /paydesk-pay-periods:
    get:
      tags:
        - PayDesk
      summary: List pay periods
      description: >-
        Returns per-employee pay periods. Read-only. Requires scope: paydesk:read.
      operationId: listPayPeriods
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - name: employeeId
          in: query
          required: false
          schema:
            type: string
          description: Return only pay periods matching this employeeId.
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: Return only pay periods matching this status.
        - name: frequency
          in: query
          required: false
          schema:
            type: string
          description: Return only pay periods matching this frequency.
      responses:
        '200':
          description: A paginated list of pay periods.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PayPeriod'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /paydesk-pay-periods/{id}:
    get:
      tags:
        - PayDesk
      summary: Retrieve a pay period
      description: >-
        Returns a single pay period by id. A record belonging to another organization
        returns 404 rather than 403. Requires scope: paydesk:read.
      operationId: getPayPeriod
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested pay period.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PayPeriod'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /team:
    get:
      tags:
        - Team
      summary: List team members
      description: >-
        Returns a cursor-paginated list of the organization's team members (the
        roster). Team membership is managed inside the Genaya app and is read-only
        here - there is no create, update, or delete, and no created-date
        (from/to) filtering. Requires scope: team:read.
      operationId: listTeamMembers
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
      responses:
        '200':
          description: A paginated list of team members.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TeamMember'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /team/{id}:
    get:
      tags:
        - Team
      summary: Retrieve a team member
      description: >-
        Returns a single team member by id. This resolves the member ids that
        appear elsewhere in the API - an appointment's assigned team, a
        commission's employeeId. A member of another organization returns 404
        rather than 403. Requires scope: team:read.
      operationId: getTeamMember
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested team member.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TeamMember'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /appointment-types:
    get:
      tags:
        - Operations
      summary: List appointment types
      description: >-
        Returns the organization's configured appointment types. Several write endpoints accept a value that must match one of these, so list them rather than hard-coding strings. Read-only - managed in the Genaya app. Requires scope: operations:read.
      operationId: listAppointmentTypes
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
      responses:
        '200':
          description: A paginated list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CatalogItem'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /appointment-types/{id}:
    get:
      tags:
        - Operations
      summary: Retrieve a appointment type
      description: >-
        Returns a single appointment type by id. A record belonging to another organization
        returns 404 rather than 403, so this can never confirm cross-tenant
        existence. Requires scope: operations:read.
      operationId: getAppointmentType
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested appointment type.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CatalogItem'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /lead-sources:
    get:
      tags:
        - Operations
      summary: List lead sources
      description: >-
        Returns the organization's configured lead sources. Several write endpoints accept a value that must match one of these, so list them rather than hard-coding strings. Read-only - managed in the Genaya app. Requires scope: operations:read.
      operationId: listLeadSources
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
      responses:
        '200':
          description: A paginated list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CatalogItem'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /lead-sources/{id}:
    get:
      tags:
        - Operations
      summary: Retrieve a lead source
      description: >-
        Returns a single lead source by id. A record belonging to another organization
        returns 404 rather than 403, so this can never confirm cross-tenant
        existence. Requires scope: operations:read.
      operationId: getLeadSource
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested lead source.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CatalogItem'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /tags:
    get:
      tags:
        - Operations
      summary: List tags
      description: >-
        Returns the organization's configured tags. Several write endpoints accept a value that must match one of these, so list them rather than hard-coding strings. Read-only - managed in the Genaya app. Requires scope: operations:read.
      operationId: listTags
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
      responses:
        '200':
          description: A paginated list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Tag'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /tags/{id}:
    get:
      tags:
        - Operations
      summary: Retrieve a tag
      description: >-
        Returns a single tag by id. A record belonging to another organization
        returns 404 rather than 403, so this can never confirm cross-tenant
        existence. Requires scope: operations:read.
      operationId: getTag
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested tag.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Tag'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /companies:
    get:
      tags:
        - Operations
      summary: List companies
      description: >-
        Returns the organization's configured companies. Several write endpoints accept a value that must match one of these, so list them rather than hard-coding strings. Read-only - managed in the Genaya app. Requires scope: operations:read.
      operationId: listCompanies
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
      responses:
        '200':
          description: A paginated list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Company'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /companies/{id}:
    get:
      tags:
        - Operations
      summary: Retrieve a company
      description: >-
        Returns a single company by id. A record belonging to another organization
        returns 404 rather than 403, so this can never confirm cross-tenant
        existence. Requires scope: operations:read.
      operationId: getCompany
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested company.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Company'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /phone-numbers:
    get:
      tags:
        - Operations
      summary: List phone numbers
      description: >-
        Returns the organization's configured phone numbers. Several write endpoints accept a value that must match one of these, so list them rather than hard-coding strings. Read-only - managed in the Genaya app. Requires scope: operations:read.
      operationId: listPhoneNumbers
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
      responses:
        '200':
          description: A paginated list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PhoneNumber'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /phone-numbers/{id}:
    get:
      tags:
        - Operations
      summary: Retrieve a phone number
      description: >-
        Returns a single phone number by id. A record belonging to another organization
        returns 404 rather than 403, so this can never confirm cross-tenant
        existence. Requires scope: operations:read.
      operationId: getPhoneNumber
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested phone number.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PhoneNumber'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /service-areas:
    get:
      tags:
        - Operations
      summary: List service areas
      description: >-
        Returns configured service areas. Polygon geometry is summarized as point counts rather than returned in full. Read-only. Requires scope: operations:read.
      operationId: listServiceAreas
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: Return only service areas matching this status.
      responses:
        '200':
          description: A paginated list of service areas.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ServiceArea'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /service-areas/{id}:
    get:
      tags:
        - Operations
      summary: Retrieve a service area
      description: >-
        Returns a single service area by id. A record belonging to another organization
        returns 404 rather than 403. Requires scope: operations:read.
      operationId: getServiceArea
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested service area.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ServiceArea'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /vendors:
    get:
      tags:
        - Operations
      summary: List vendors
      description: >-
        Returns the organization's vendors. Read-only. Requires scope: operations:read.
      operationId: listVendors
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: Return only vendors matching this status.
        - name: vendorsType
          in: query
          required: false
          schema:
            type: string
          description: Return only vendors matching this vendorsType.
      responses:
        '200':
          description: A paginated list of vendors.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Vendor'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /vendors/{id}:
    get:
      tags:
        - Operations
      summary: Retrieve a vendor
      description: >-
        Returns a single vendor by id. A record belonging to another organization
        returns 404 rather than 403. Requires scope: operations:read.
      operationId: getVendor
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested vendor.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Vendor'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /items-and-services:
    get:
      tags:
        - Operations
      summary: List items and services
      description: >-
        Returns the price book, including stock levels for tracked products. Read-only. Requires scope: operations:read.
      operationId: listItemsAndServices
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
        - name: itemType
          in: query
          required: false
          schema:
            type: string
          description: Return only items and services matching this itemType.
        - name: categoryId
          in: query
          required: false
          schema:
            type: string
          description: Return only items and services matching this categoryId.
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: Return only items and services matching this status.
      responses:
        '200':
          description: A paginated list of items and services.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ItemOrService'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /items-and-services/{id}:
    get:
      tags:
        - Operations
      summary: Retrieve a item or service
      description: >-
        Returns a single item or service by id. A record belonging to another organization
        returns 404 rather than 403. Requires scope: operations:read.
      operationId: getItemOrService
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested item or service.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ItemOrService'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /item-categories:
    get:
      tags:
        - Operations
      summary: List item categories
      description: >-
        Returns categories used to group items and services. Read-only. Requires scope: operations:read.
      operationId: listItemCategories
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
      responses:
        '200':
          description: A paginated list of item categories.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CatalogItem'
                  has_more:
                    type: boolean
                  next_cursor:
                    type: string
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /item-categories/{id}:
    get:
      tags:
        - Operations
      summary: Retrieve a item category
      description: >-
        Returns a single item category by id. A record belonging to another organization
        returns 404 rather than 403. Requires scope: operations:read.
      operationId: getItemCategory
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested item category.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CatalogItem'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
  /account:
    get:
      tags:
        - Account
      summary: Retrieve the calling key's account
      description: >-
        Returns which organization the key belongs to, the plan, and what the key
        itself is allowed to do. No scope is required - a key must always be able
        to describe itself, otherwise debugging a 403 just produces another 403.
      operationId: getAccount
      responses:
        '200':
          description: The account behind this API key.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Account'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'

  /usage:
    get:
      tags:
        - Account
      summary: Retrieve current usage and limits
      description: >-
        Returns consumption against every limit that can reject a request - the
        per-minute and per-day rate limit for this key, the organization's daily
        write quota, and any countable plan cap. No scope is required. Use this to
        turn a 429 into something self-diagnosable.
      operationId: getUsage
      responses:
        '200':
          description: Current usage and limits.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Usage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/ServerError'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "A Genaya API key, sent in the Authorization header as Bearer gny_live_xxx."
  parameters:
    Limit:
      name: limit
      in: query
      required: false
      description: Maximum number of items to return (default 25, max 100).
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    StartingAfter:
      name: starting_after
      in: query
      required: false
      description: A cursor - the id of the last item from the previous page. Returns the items after it.
      schema:
        type: string
    From:
      name: from
      in: query
      required: false
      description: >-
        Filter to resources created on or after this date. Accepts an ISO-8601
        date (2026-06-01, snapped to the start of the UTC day) or a full
        timestamp (2026-06-01T00:00:00Z). When set, results are ordered
        newest-first by created date.
      schema:
        type: string
        format: date-time
    To:
      name: to
      in: query
      required: false
      description: >-
        Filter to resources created on or before this date. Accepts an ISO-8601
        date (2026-06-30, snapped to the end of the UTC day) or a full timestamp
        (2026-06-30T23:59:59Z). When set, results are ordered newest-first by
        created date.
      schema:
        type: string
        format: date-time
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Send a unique string to safely retry a create. The first response is replayed for 24h; reusing the key with a different body returns 409.
      schema:
        type: string
  responses:
    BadRequest:
      description: Malformed or missing parameters (type invalid_request_error).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing, malformed, or revoked API key (type authentication_error).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: The key lacks the required scope, or the organization is not on the Business plan (type permission_error).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: No such resource in this organization (type not_found).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Conflict:
      description: The Idempotency-Key was reused with a different request body (type idempotency_error).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    UnprocessableEntity:
      description: Well-formed but rejected by a business rule, e.g. a required-field configuration (type validation_error).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    TooManyRequests:
      description: Rate limited (type rate_limit_error). See the Retry-After header.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ServerError:
      description: Unexpected server error (type api_error).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      description: The Stripe-style error envelope returned by every failed request.
      properties:
        error:
          type: object
          properties:
            type:
              type: string
              description: The error category, mapped to the HTTP status.
              example: invalid_request_error
            code:
              type: string
              description: A short machine-readable code.
              example: missing_field
            message:
              type: string
              description: A human-readable explanation.
              example: clientName is required.
            param:
              type: string
              nullable: true
              description: The offending field, when applicable.
      example:
        error:
          type: invalid_request_error
          code: missing_field
          message: clientName is required.
          param: clientName
    Address:
      type: object
      description: >-
        Canonical address sub-object. Emitted only when at least one address field
        is present; individual keys are omitted when empty.
      properties:
        line1:
          type: string
          description: Street address line.
          example: 123 Market St
        unit:
          type: string
          description: Unit, apartment, or suite.
          example: Suite 200
        city:
          type: string
          example: Austin
        state:
          type: string
          example: TX
        zip:
          type: string
          example: "78701"
        country:
          type: string
          example: US
    AppointmentTeamMember:
      type: object
      description: A team member assigned to the appointment.
      properties:
        id:
          type: string
          nullable: true
          description: Team member id (teamId).
          example: tm_8c1f0a
        name:
          type: string
          nullable: true
          example: Jordan Rivera
    Appointment:
      type: object
      description: A scheduled appointment / project / engagement for the organization.
      properties:
        id:
          type: string
          description: Unique appointment id.
          example: 9Kd2pXq7Lm4
        object:
          type: string
          description: The object type. Always "appointment".
          example: appointment
        appointmentId:
          type: integer
          nullable: true
          description: >-
            The organization's sequential numeric appointment id (the number shown in
            the app), distinct from the opaque `id`. Null when numbering was
            unavailable at create time.
          example: 1042
        uuid:
          type: string
          nullable: true
          description: Short human-shareable appointment code.
          example: K7QF3M
        clientId:
          type: string
          nullable: true
          description: Id of the linked client record.
          example: 9Kd2pXq7Lm4
        clientFullName:
          type: string
          description: Full client/patient name (explicit or derived from first + last).
          example: Dana Whitfield
        firstName:
          type: string
          example: Dana
        lastName:
          type: string
          example: Whitfield
        company:
          type: string
          example: Whitfield HVAC
        phone:
          type: string
          example: "+15125550101"
        secondaryPhone:
          type: string
          example: "+15125550102"
        email:
          type: string
          example: dana@example.com
        appointmentType:
          type: string
          example: Diagnostic Visit
        appointmentTypeId:
          type: string
          nullable: true
          example: at_diag_01
        leadSource:
          type: string
          nullable: true
          example: Google
        leadSourceId:
          type: string
          nullable: true
          example: ls_google
        description:
          type: string
          example: AC not cooling on the second floor.
        address:
          $ref: '#/components/schemas/Address'
        fullAddress:
          type: string
          example: 123 Market St, Austin, TX, 78701, US
        serviceArea:
          type: string
          nullable: true
          example: North Austin
        serviceAreaId:
          type: string
          nullable: true
          example: sa_north
        companyId:
          type: string
          nullable: true
          description: Id of the organization company the appointment is billed from.
          example: co_main
        companyName:
          type: string
          nullable: true
          description: Display name of the billed-from organization company.
          example: Whitfield HVAC LLC
        team:
          type: array
          description: Team members assigned to the appointment.
          items:
            $ref: '#/components/schemas/AppointmentTeamMember'
        status:
          type: string
          nullable: true
          example: Scheduled
        tags:
          type: array
          description: Tag names/ids applied to the appointment.
          items:
            type: string
          example:
            - VIP
            - Follow-up
        customFields:
          type: object
          description: Organization-defined custom field values, keyed by field id.
          additionalProperties: true
          example:
            cf_priority: High
        startAt:
          type: string
          format: date-time
          nullable: true
          description: Appointment start time (ISO-8601).
          example: "2026-06-20T15:00:00.000Z"
        endAt:
          type: string
          format: date-time
          nullable: true
          description: Appointment end time (ISO-8601).
          example: "2026-06-20T16:00:00.000Z"
        assignedUserIds:
          type: array
          items: { type: string }
          description: Team-member ids assigned to this appointment (read-only).
        lineItems:
          type: array
          description: Billable line items, present once the appointment carries items (read-only).
          items:
            type: object
            properties:
              entryId: { type: string }
              itemId: { type: string }
              name: { type: string }
              description: { type: string }
              quantity: { type: number }
              unitPrice: { type: number }
              unitCost: { type: number }
              taxable: { type: boolean }
              taxRate: { type: number }
              amount: { type: number }
              type: { type: string }
        subtotal: { type: number, description: "Sum of line items (read-only)." }
        taxTotal: { type: number, description: "Tax total (read-only)." }
        total: { type: number, description: "Grand total (read-only)." }
        balance: { type: number, description: "Outstanding balance (read-only)." }
        appliedTaxId: { type: string, description: "Applied tax id (read-only)." }
        appliedTaxName: { type: string, description: "Applied tax name (read-only)." }
        taxRate: { type: number, description: "Applied tax rate fraction (read-only)." }
        employeeCost: { type: number, description: "Team cost (read-only)." }
        companyCost: { type: number, description: "Company cost (read-only)." }
        currency: { type: string, description: "Currency code (read-only unless set)." }
        terms: { type: number, description: "Payment terms in days (read-only unless set)." }
        notes:
          type: array
          description: Appointment notes (read-only).
          items:
            type: object
            properties:
              text: { type: string }
              createdBy: { type: string }
              createdAt: { type: string, format: date-time }
        lastInvoiceId: { type: string, description: "Doc id of the most recent invoice generated from this appointment (read-only)." }
        lastInvoicedAt: { type: string, format: date-time, nullable: true, description: "When the last invoice was generated (read-only)." }
        lastEstimateId: { type: string, description: "Doc id of the most recent estimate generated (read-only)." }
        lastEstimatedAt: { type: string, format: date-time, nullable: true, description: "When the last estimate was generated (read-only)." }
        createdByName:
          type: string
          nullable: true
          description: Display name of whoever created the appointment.
        latitude:
          type: number
          nullable: true
          description: Latitude of the appointment address, when geocoded.
        longitude:
          type: number
          nullable: true
          description: Longitude of the appointment address, when geocoded.

        createdAt:
          type: string
          format: date-time
          nullable: true
          description: When the appointment was created (ISO-8601).
          example: "2026-06-17T12:00:00.000Z"
        updatedAt:
          type: string
          format: date-time
          nullable: true
          description: When the appointment was last updated (ISO-8601).
          example: "2026-06-17T12:30:00.000Z"
      example:
        id: 9Kd2pXq7Lm4
        object: appointment
        appointmentId: 1042
        uuid: K7QF3M
        clientId: 4Bf1nWq2Jp9
        clientFullName: Dana Whitfield
        firstName: Dana
        lastName: Whitfield
        company: Whitfield HVAC
        phone: "+15125550101"
        email: dana@example.com
        appointmentType: Diagnostic Visit
        appointmentTypeId: at_diag_01
        description: AC not cooling on the second floor.
        address:
          line1: 123 Market St
          city: Austin
          state: TX
          zip: "78701"
          country: US
        fullAddress: 123 Market St, Austin, TX, 78701, US
        serviceArea: North Austin
        serviceAreaId: sa_north
        companyId: co_main
        companyName: Whitfield HVAC LLC
        team:
          - id: tm_8c1f0a
            name: Jordan Rivera
        status: Scheduled
        tags:
          - VIP
          - Follow-up
        customFields:
          cf_priority: High
        startAt: "2026-06-20T15:00:00.000Z"
        endAt: "2026-06-20T16:00:00.000Z"
        createdAt: "2026-06-17T12:00:00.000Z"
        updatedAt: "2026-06-17T12:30:00.000Z"
    AppointmentInput:
      type: object
      description: >-
        Fields accepted when creating or updating an appointment. On create, only
        organization-configured required fields are enforced; on update, only the
        supplied fields are changed. organizationId and the actor are derived from
        the API key context and are never accepted in the body.
      properties:
        clientId:
          type: string
          description: >-
            Link the appointment to an existing client by id. When omitted, a client
            is upserted from the supplied contact fields.
          example: 4Bf1nWq2Jp9
        clientFullName:
          type: string
          description: >-
            Full client name. When omitted, it is computed from firstName +
            lastName.
          example: Dana Whitfield
        firstName:
          type: string
          example: Dana
        lastName:
          type: string
          example: Whitfield
        company:
          type: string
          example: Whitfield HVAC
        phone:
          type: string
          example: "+15125550101"
        secondaryPhone:
          type: string
          example: "+15125550102"
        email:
          type: string
          example: dana@example.com
        appointmentType:
          type: string
          example: Diagnostic Visit
        appointmentTypeId:
          type: string
          example: at_diag_01
        leadSource:
          type: string
          example: Google
        leadSourceId:
          type: string
          example: ls_google
        description:
          type: string
          example: AC not cooling on the second floor.
        address:
          $ref: '#/components/schemas/Address'
        fullAddress:
          type: string
          description: >-
            Full address string. When omitted, it is recomputed from the address
            sub-object.
          example: 123 Market St, Austin, TX, 78701, US
        serviceArea:
          type: string
          example: North Austin
        serviceAreaId:
          type: string
          example: sa_north
        companyId:
          type: string
          description: Id of the organization company to bill the appointment from.
          example: co_main
        companyName:
          type: string
          description: Display name of the billed-from organization company.
          example: Whitfield HVAC LLC
        team:
          type: array
          description: Team members to assign. Each item needs an id and/or name.
          items:
            $ref: '#/components/schemas/AppointmentTeamMember'
        status:
          type: string
          example: Scheduled
        tags:
          type: array
          description: Tag names/ids to apply to the appointment.
          items:
            type: string
          example:
            - VIP
            - Follow-up
        customFields:
          type: object
          additionalProperties: true
          example:
            cf_priority: High
        startAt:
          type: string
          format: date-time
          description: Appointment start time (ISO-8601, Date, or epoch millis).
          example: "2026-06-20T15:00:00.000Z"
        endAt:
          type: string
          format: date-time
          description: Appointment end time (ISO-8601, Date, or epoch millis).
          example: "2026-06-20T16:00:00.000Z"
      example:
        firstName: Dana
        lastName: Whitfield
        phone: "+15125550101"
        email: dana@example.com
        appointmentType: Diagnostic Visit
        description: AC not cooling on the second floor.
        address:
          line1: 123 Market St
          city: Austin
          state: TX
          zip: "78701"
          country: US
        team:
          - id: tm_8c1f0a
            name: Jordan Rivera
        status: Scheduled
        startAt: "2026-06-20T15:00:00.000Z"
        endAt: "2026-06-20T16:00:00.000Z"
    DeletedAppointment:
      type: object
      description: Confirmation returned when an appointment is soft-deleted.
      properties:
        id:
          type: string
          example: 9Kd2pXq7Lm4
        object:
          type: string
          example: appointment
        deleted:
          type: boolean
          example: true
    Client:
      type: object
      description: >-
        A client (also referred to as a patient/customer depending on industry) in
        your Genaya organization. Created via the API through the same upsert path as
        the app: the create endpoint matches an existing active client by email, then
        phone, then secondary phone - so posting a contact that matches an existing
        client UPDATES that client rather than creating a duplicate.
      properties:
        id:
          type: string
          description: Unique identifier for the client.
          example: 7Qx2mF0aBcDeFgHiJkLm
        object:
          type: string
          description: The object type. Always "client".
          example: client
        numericId:
          type: integer
          nullable: true
          description: >-
            Org-scoped sequential client number assigned from the organization's
            numbering settings. Distinct from the string `id` (the record handle).
            Null until a number has been allocated.
          example: 1042
        uuid:
          type: string
          description: Stable short UUID assigned to the client record.
          example: K7M3PQ
        title:
          type: string
          description: Salutation or title (e.g. Mr., Dr.).
          example: Dr.
        firstName:
          type: string
          example: Jordan
        lastName:
          type: string
          example: Avery
        fullName:
          type: string
          description: Full display name as stored on the record.
          example: Jordan Avery
        company:
          type: string
          example: Avery Industries
        phone:
          type: string
          description: Primary phone number in E.164 or stored format.
          example: "+14155550142"
        secondaryPhone:
          type: string
          example: "+14155550199"
        email:
          type: string
          format: email
          example: jordan.avery@example.com
        phoneNumbers:
          type: array
          description: >-
            All phone numbers on the record, derived from the primary and secondary
            phone. The primary phone is flagged isPrimary. Read-only - managed via
            the phone and secondaryPhone fields.
          items:
            $ref: '#/components/schemas/ClientPhoneNumber'
        address:
          $ref: '#/components/schemas/ClientAddress'
        fullAddress:
          type: string
          nullable: true
          description: >-
            Derived single-line address (line1, city, state, zip joined by commas).
            Read-only - never accepted on create/update. Null when no address is set.
          example: 500 Market St, San Francisco, CA, 94105
        leadSource:
          type: string
          nullable: true
          description: How the client was acquired. Null when not set.
          example: Website
        leadSourceId:
          type: string
          nullable: true
          description: >-
            Identifier of the lead source. Mirrors leadSource through the shared
            upsert path. Null when not set.
          example: Website
        status:
          type: string
          description: Lifecycle status. Defaults to "active".
          example: active
        customFields:
          type: object
          additionalProperties: true
          description: Organization-defined custom field values keyed by field id.
          example:
            preferredContact: email
            accountTier: gold
        organizationCompanyId:
          type: string
          nullable: true
          description: Identifier of the company/branch the client is associated with. Null when unset.
          example: cmp_9aF2
        organizationCompanyName:
          type: string
          nullable: true
          description: Display name of the associated company/branch. Null when unset.
          example: Avery HVAC - North
        organizationCompanyLogoUrl:
          type: string
          nullable: true
          description: Logo URL of the associated company/branch. Null when unset.
          example: https://storage.genaya.com/orgs/logos/north.png
        createdAt:
          type: string
          format: date-time
          nullable: true
          example: "2026-06-17T14:32:00.000Z"
        updatedAt:
          type: string
          format: date-time
          nullable: true
          example: "2026-06-17T15:10:00.000Z"
      required:
        - id
        - object
      example:
        id: 7Qx2mF0aBcDeFgHiJkLm
        object: client
        numericId: 1042
        uuid: K7M3PQ
        title: Dr.
        firstName: Jordan
        lastName: Avery
        fullName: Jordan Avery
        company: Avery Industries
        phone: "+14155550142"
        secondaryPhone: "+14155550199"
        email: jordan.avery@example.com
        phoneNumbers:
          - number: "+14155550142"
            normalized: "14155550142"
            isPrimary: true
          - number: "+14155550199"
            normalized: "14155550199"
            isPrimary: false
        address:
          line1: 500 Market St
          unit: Suite 400
          city: San Francisco
          state: CA
          zip: "94105"
          country: US
        fullAddress: 500 Market St, San Francisco, CA, 94105
        leadSource: Website
        leadSourceId: Website
        status: active
        customFields:
          preferredContact: email
        organizationCompanyId: cmp_9aF2
        organizationCompanyName: Avery HVAC - North
        organizationCompanyLogoUrl: https://storage.genaya.com/orgs/logos/north.png
        createdAt: "2026-06-17T14:32:00.000Z"
        updatedAt: "2026-06-17T15:10:00.000Z"

    ClientAddress:
      type: object
      description: >-
        Canonical address sub-object. Individual keys are omitted when empty, and the
        whole object is omitted when no address fields are set.
      properties:
        line1:
          type: string
          example: 500 Market St
        unit:
          type: string
          example: Suite 400
        city:
          type: string
          example: San Francisco
        state:
          type: string
          example: CA
        zip:
          type: string
          example: "94105"
        country:
          type: string
          example: US

    ClientPhoneNumber:
      type: object
      description: >-
        A single phone entry on the client. Derived from the primary and secondary
        phone numbers; read-only on the public API.
      properties:
        number:
          type: string
          description: The phone number as entered.
          example: "+14155550142"
        normalized:
          type: string
          nullable: true
          description: Digits-only normalization of the number. Null when it could not be derived.
          example: "14155550142"
        isPrimary:
          type: boolean
          description: Whether this is the client's primary phone.
          example: true

    ClientCreate:
      type: object
      description: >-
        Request body to create a client. Provide at least one of firstName, lastName,
        company, email, or phone. If the contact matches an existing active client (by
        email, then phone, then secondary phone) that client is updated instead.
        Derived fields (fullName, fullAddress, phoneNumbers, leadSourceId, numericId)
        are computed server-side and cannot be set here.
      properties:
        uuid:
          type: string
          description: Optional short UUID to assign. One is generated when omitted.
          example: K7M3PQ
        title:
          type: string
          example: Dr.
        firstName:
          type: string
          example: Jordan
        lastName:
          type: string
          example: Avery
        company:
          type: string
          example: Avery Industries
        phone:
          type: string
          example: "+14155550142"
        secondaryPhone:
          type: string
          example: "+14155550199"
        email:
          type: string
          format: email
          example: jordan.avery@example.com
        address:
          $ref: '#/components/schemas/ClientAddress'
        leadSource:
          type: string
          description: How the client was acquired. Also populates leadSourceId.
          example: Website
        status:
          type: string
          description: Lifecycle status. Lowercased on save.
          example: active
        customFields:
          type: object
          additionalProperties: true
          example:
            preferredContact: email
        organizationCompanyId:
          type: string
          description: Identifier of the company/branch to associate the client with.
          example: cmp_9aF2
        organizationCompanyName:
          type: string
          description: Display name of the associated company/branch.
          example: Avery HVAC - North
        organizationCompanyLogoUrl:
          type: string
          description: Logo URL of the associated company/branch.
          example: https://storage.genaya.com/orgs/logos/north.png
      example:
        firstName: Jordan
        lastName: Avery
        email: jordan.avery@example.com
        phone: "+14155550142"
        address:
          line1: 500 Market St
          unit: Suite 400
          city: San Francisco
          state: CA
          zip: "94105"
          country: US
        leadSource: Website

    ClientUpdate:
      type: object
      description: >-
        Request body to update a client. All fields optional; only provided fields are
        applied through the shared upsert path. Derived fields (fullName, fullAddress,
        phoneNumbers, leadSourceId, numericId) are computed server-side and cannot be set.
      properties:
        uuid:
          type: string
        title:
          type: string
        firstName:
          type: string
        lastName:
          type: string
        company:
          type: string
        phone:
          type: string
        secondaryPhone:
          type: string
        email:
          type: string
          format: email
        address:
          $ref: '#/components/schemas/ClientAddress'
        leadSource:
          type: string
        status:
          type: string
        customFields:
          type: object
          additionalProperties: true
        organizationCompanyId:
          type: string
        organizationCompanyName:
          type: string
        organizationCompanyLogoUrl:
          type: string
      example:
        company: Avery Industries LLC
        status: active

    ClientDeleted:
      type: object
      description: Returned when a client is soft-deleted. Portal access is revoked.
      properties:
        id:
          type: string
          example: 7Qx2mF0aBcDeFgHiJkLm
        object:
          type: string
          example: client
        deleted:
          type: boolean
          example: true
      required:
        - id
        - object
        - deleted
    Contact:
      type: object
      description: A contact record (call center / chats / rooms directory entry).
      properties:
        id:
          type: string
          description: Unique identifier of the contact.
        object:
          type: string
          description: The object type. Always "contact".
          example: contact
        name:
          type: string
          description: Display name of the contact. Empty string when unset.
        phone:
          type: string
          description: Primary phone number. Empty string when unset.
        email:
          type: string
          description: Email address. Empty string when unset.
        company:
          type: string
          description: Company or organization name for the contact. Empty string when unset.
        avatarUrl:
          type: string
          description: URL of the contact's avatar image. Empty string when unset.
        entity:
          type: string
          description: >-
            Which surface(s) this contact belongs to. Any unrecognized or missing
            value normalizes to "both".
          enum:
            - chats
            - rooms
            - callCenter
            - both
        address:
          nullable: true
          $ref: '#/components/schemas/ContactAddress'
        notes:
          type: array
          description: Embedded notes attached to the contact.
          items:
            $ref: '#/components/schemas/ContactNote'
        createdAt:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 timestamp when the contact was created.
        updatedAt:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 timestamp when the contact was last updated.
      example:
        id: ct_8fJ2kLqWxr
        object: contact
        name: Jane Doe
        phone: "+14155551234"
        email: jane@example.com
        company: Acme Clinic
        avatarUrl: ""
        entity: callCenter
        address:
          address: 123 Market St
          unit: Suite 200
          city: San Francisco
          state: CA
          zipCode: "94103"
          country: USA
          lat: 37.7793
          lng: -122.4193
          fullAddress: 123 Market St Suite 200, San Francisco, CA, 94103, USA
        notes:
          - id: nt_1
            text: Prefers afternoon callbacks.
            createdBy: usr_abc123
            createdByName: Alex Operator
            createdAt: "2026-06-17T15:04:05.000Z"
        createdAt: "2026-06-17T15:00:00.000Z"
        updatedAt: "2026-06-17T15:04:05.000Z"
    ContactAddress:
      type: object
      description: >-
        Structured address for a contact. fullAddress is read-only and recomputed
        server-side from the parts on every write.
      properties:
        address:
          type: string
          description: Street address line.
        unit:
          type: string
          description: Unit, suite, or apartment.
        city:
          type: string
        state:
          type: string
        zipCode:
          type: string
        country:
          type: string
        lat:
          type: number
          nullable: true
          description: Latitude, or null when unset.
        lng:
          type: number
          nullable: true
          description: Longitude, or null when unset.
        fullAddress:
          type: string
          description: Server-computed single-line address. Read-only.
    ContactNote:
      type: object
      description: An embedded note on a contact.
      properties:
        id:
          type: string
        text:
          type: string
        createdBy:
          type: string
          nullable: true
          description: Identifier of the member who created the note.
        createdByName:
          type: string
          nullable: true
          description: Display name of the member who created the note.
        createdAt:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 timestamp when the note was created.
    ContactInput:
      type: object
      description: >-
        Request body for creating or updating a contact. On create, at least one of
        name or phone is required. On update, every field is optional and only the
        supplied fields are changed.
      properties:
        name:
          type: string
          description: Display name. Max 120 characters.
        phone:
          type: string
          description: Primary phone number. Max 32 characters.
        email:
          type: string
          description: Email address. Max 120 characters.
        company:
          type: string
          description: Company or organization name. Max 120 characters.
        avatarUrl:
          type: string
          description: URL of the contact's avatar image. Max 600 characters.
        entity:
          type: string
          description: >-
            Which surface(s) this contact belongs to. Any unrecognized value
            normalizes to "both".
          enum:
            - chats
            - rooms
            - callCenter
            - both
        address:
          $ref: '#/components/schemas/ContactAddressInput'
      example:
        name: Jane Doe
        phone: "+14155551234"
        email: jane@example.com
        company: Acme Clinic
        entity: callCenter
        address:
          address: 123 Market St
          unit: Suite 200
          city: San Francisco
          state: CA
          zipCode: "94103"
          country: USA
          lat: 37.7793
          lng: -122.4193
    ContactAddressInput:
      type: object
      description: >-
        Address parts supplied on write. fullAddress is ignored on input and always
        recomputed server-side from these parts.
      properties:
        address:
          type: string
          description: Street address line. Max 240 characters.
        unit:
          type: string
          description: Unit, suite, or apartment. Max 60 characters.
        city:
          type: string
          description: Max 120 characters.
        state:
          type: string
          description: Max 120 characters.
        zipCode:
          type: string
          description: Postal code. Max 20 characters. The alias "zip" is also accepted.
        country:
          type: string
          description: Max 120 characters.
        lat:
          type: number
          nullable: true
          description: Latitude. Stored as null when not a finite number.
        lng:
          type: number
          nullable: true
          description: Longitude. Stored as null when not a finite number.
    DeletedContact:
      type: object
      description: Returned by the delete endpoint to confirm a soft delete.
      properties:
        id:
          type: string
          description: Identifier of the deleted contact.
        object:
          type: string
          example: contact
        deleted:
          type: boolean
          example: true
    LeadTable:
      type: object
      description: >-
        A lead table - the schema document that defines the tabs and columns every
        lead row in that table is stored against. Read-only over the public API.
        Soft-deleted tabs and columns are omitted, and tenant/soft-delete
        bookkeeping (organizationId, archivedBy, deletedAt, deletedBy) is never
        exposed. The columns array is a flattened, back-compat mirror of every
        non-deleted column across every non-deleted tab.
      properties:
        id:
          type: string
          description: Unique identifier for the lead table.
          example: 9KdxQwErTyUiOpAsDfGh
        object:
          type: string
          description: The object type. Always "lead_table".
          example: lead_table
        name:
          type: string
          description: Human-readable name of the lead table.
          example: Inbound Web Leads
        createdBy:
          type: string
          nullable: true
          description: Id of the team member who created the table.
          example: usr_8f3a1c
        assignedUserIds:
          type: array
          description: Team member ids the table is shared with.
          items:
            type: string
          example:
            - usr_8f3a1c
            - usr_2b9d77
        assignments:
          type: array
          description: >-
            Resolved details for each assigned team member (id, email, name), as
            stored on the table at assign time.
          items:
            type: object
            properties:
              id:
                type: string
                description: Team member id.
                example: usr_8f3a1c
              email:
                type: string
                description: Team member email.
                example: rep@acme.com
              name:
                type: string
                description: Team member display name.
                example: Jordan Rivera
        folderIds:
          type: array
          description: Ids of the lead-table folders this table is filed under.
          items:
            type: string
          example:
            - fld_inbound
        starred:
          type: boolean
          description: Whether the table is marked as a favorite.
          example: false
        archived:
          type: boolean
          description: Whether the table is archived (hidden from the default view).
          example: false
        archivedAt:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 timestamp of when the table was archived, or null.
          example: null
        tabs:
          type: array
          description: >-
            The table's tabs (sub-sections), in storage order. Soft-deleted tabs are
            omitted.
          items:
            $ref: '#/components/schemas/LeadTableTab'
        columns:
          type: array
          description: >-
            Back-compat flattened list of every non-deleted column across every
            non-deleted tab, in tab-then-column order.
          items:
            $ref: '#/components/schemas/LeadTableColumn'
        createdAt:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 timestamp of when the table was created.
          example: '2026-04-12T15:04:21.000Z'
        updatedAt:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 timestamp of when the table was last updated.
          example: '2026-05-30T09:18:47.000Z'
      required:
        - id
        - object
        - tabs
        - columns
      example:
        id: 9KdxQwErTyUiOpAsDfGh
        object: lead_table
        name: Inbound Web Leads
        createdBy: usr_8f3a1c
        assignedUserIds:
          - usr_8f3a1c
        assignments:
          - id: usr_8f3a1c
            email: rep@acme.com
            name: Jordan Rivera
        folderIds:
          - fld_inbound
        starred: false
        archived: false
        archivedAt: null
        tabs:
          - id: tab_main
            name: Tab 1
            displayOrder: 1
            columns:
              - id: col_name
                nameId: firstName
                name: First Name
                type: text
                collectionType: system
                visible: true
                frozen: true
                locked: false
                hidden: false
                displayOrder: 1
              - id: col_stage
                nameId: stage
                name: Stage
                type: single_select
                collectionType: custom
                visible: true
                frozen: false
                locked: false
                hidden: false
                displayOrder: 2
                options:
                  - id: opt_new
                    name: New
                    displayOrder: 1
                    color: '#60a5fa'
                  - id: opt_won
                    name: Won
                    displayOrder: 2
                    color: '#22c55e'
        columns:
          - id: col_name
            nameId: firstName
            name: First Name
            type: text
            collectionType: system
            visible: true
            frozen: true
            locked: false
            hidden: false
            displayOrder: 1
        createdAt: '2026-04-12T15:04:21.000Z'
        updatedAt: '2026-05-30T09:18:47.000Z'

    LeadTableTab:
      type: object
      description: >-
        A tab (sub-section) of a lead table. Each tab holds its own ordered column
        schema. Soft-deleted columns are omitted.
      properties:
        id:
          type: string
          description: Stable identifier for the tab.
          example: tab_main
        name:
          type: string
          description: Display name of the tab.
          example: Tab 1
        displayOrder:
          type: integer
          description: Ordering position of the tab within the table.
          example: 1
        columns:
          type: array
          description: The columns that make up this tab's schema, in definition order.
          items:
            $ref: '#/components/schemas/LeadTableColumn'
      required:
        - id
        - columns

    LeadTableColumn:
      type: object
      description: >-
        A single column in a lead table schema. The options array is present only
        for single_select and multi_select column types; the date/time, number, and
        phone formatting fields are present only for their matching column type.
      properties:
        id:
          type: string
          description: Stable identifier for the column.
          example: col_stage
        nameId:
          type: string
          description: >-
            Stable key that lead rows store this column's value under. Survives
            renames.
          example: stage
        name:
          type: string
          description: Display name of the column.
          example: Stage
        type:
          type: string
          description: >-
            The column data type (for example text, short_text, long_text, number,
            phone, email, date_time, single_select, multi_select, assignee,
            attachment, rating, checkbox).
          example: single_select
        collectionType:
          type: string
          description: >-
            How the column was created - "system" for the built-in columns or
            "custom" for user-added ones.
          example: custom
        visible:
          type: boolean
          description: Whether the column is shown in the table.
          example: true
        frozen:
          type: boolean
          description: Whether the column is pinned (frozen) to the left of the grid.
          example: false
        locked:
          type: boolean
          description: Whether the column is locked against value edits.
          example: false
        hidden:
          type: boolean
          description: Whether the column is hidden from the table view.
          example: false
        displayOrder:
          type: integer
          description: Ordering position of the column within its tab.
          example: 2
        options:
          type: array
          description: >-
            Selectable options. Present only when type is single_select or
            multi_select.
          items:
            type: object
            properties:
              id:
                type: string
                description: Stable identifier for the option.
                example: opt_new
              name:
                type: string
                description: Display label for the option.
                example: New
              displayOrder:
                type: integer
                description: Ordering position of the option.
                example: 1
              color:
                type: string
                description: Hex color swatch for the option.
                example: '#60a5fa'
        dateTimeMode:
          type: string
          description: >-
            Date/time granularity (date, time, or date_time). Present only when type
            is date_time.
          example: date
        dateTimeFormat:
          type: string
          description: >-
            Display format string for the value. Present only when type is
            date_time.
          example: MM/DD/YYYY
        numberPrecision:
          type: integer
          description: >-
            Number of decimal places (0-4). Present only when type is number.
          example: 2
        currencyEnabled:
          type: boolean
          description: >-
            Whether the number is formatted as a currency. Present only when type is
            number.
          example: false
        currencyCode:
          type: string
          description: >-
            ISO 4217 currency code when currency formatting is enabled. Present only
            when type is number.
          example: USD
        currencyName:
          type: string
          description: >-
            Human-readable currency name when currency formatting is enabled.
            Present only when type is number.
          example: US Dollar
        currencySymbol:
          type: string
          description: >-
            Currency symbol when currency formatting is enabled. Present only when
            type is number.
          example: $
        phoneFormatEnabled:
          type: boolean
          description: >-
            Whether phone-number formatting is applied. Present only when type is
            phone.
          example: true
        phoneAreaCode:
          type: string
          description: >-
            Default dialing/area code when phone formatting is enabled. Present only
            when type is phone.
          example: '+1'
        phoneFormat:
          type: string
          description: >-
            Phone display format identifier when formatting is enabled. Present only
            when type is phone.
          example: intl-dash
      required:
        - id
        - name
        - type

    LeadTableCreate:
      type: object
      description: Parameters for creating a lead table.
      required:
        - name
      properties:
        name:
          type: string
          maxLength: 200
          description: The table's display name.
          example: Q3 Inbound Leads
        columns:
          type: array
          maxItems: 50
          description: >-
            Additional columns beyond the two system columns every table receives.
            Types needing extra configuration (select options, date format, currency,
            phone format) are created in their plain form - configure them in the app.
          items:
            type: object
            required:
              - name
            properties:
              name:
                type: string
                description: Column label. A stable nameId is derived from it.
              type:
                type: string
                default: text
                enum:
                  - text
                  - long_text
                  - number
                  - date_time
                  - phone
                  - email
                  - url
                  - checkbox
                  - single_select
                  - multi_select
    Lead:
      type: object
      description: >-
        A lead row inside a lead table. Reserved keys are surfaced as named fields;
        all dynamic column cells are collapsed into the `fields` map.
      properties:
        id:
          type: string
          description: Unique identifier for the lead row.
        object:
          type: string
          description: The object type. Always "lead".
          example: lead
        tableId:
          type: string
          description: The id of the parent lead table this row belongs to.
        tabId:
          type: string
          nullable: true
          description: The id of the tab this row is filed under, or null.
        fields:
          type: object
          additionalProperties: true
          description: >-
            Dynamic column cell values keyed by each column's nameId. Values are
            surfaced raw (string, number, array, etc.) since the column type is not
            coerced here.
        starred:
          type: boolean
          description: Whether the row is starred. Defaults to false.
        displayOrder:
          type: number
          description: >-
            Manual sort position within the table. Omitted when the row has no
            explicit display order.
        createdAt:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 timestamp when the row was created.
        updatedAt:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 timestamp when the row was last updated.
      required:
        - id
        - object
        - tableId
        - fields
        - starred
      example:
        id: lead_9fQ2x7TzKp
        object: lead
        tableId: tbl_3KdR8mVnQ1
        tabId: tab_new
        fields:
          firstName: Jordan
          lastName: Rivera
          email: jordan.rivera@example.com
          phone: "+15551234567"
        starred: false
        displayOrder: 1
        createdAt: '2026-06-17T14:32:10.000Z'
        updatedAt: '2026-06-17T14:32:10.000Z'
    LeadCreate:
      type: object
      description: Request body for creating a lead row.
      properties:
        tabId:
          type: string
          description: >-
            Optional tab to file the row under. Must match a tab on the table;
            otherwise the table's first tab is used.
        fields:
          type: object
          additionalProperties: true
          description: >-
            Dynamic column cell values keyed by each column's nameId. Stored
            verbatim. Reserved keys are ignored.
        displayOrder:
          type: number
          description: Optional manual sort position within the table.
      example:
        tabId: tab_new
        fields:
          firstName: Jordan
          lastName: Rivera
          email: jordan.rivera@example.com
          phone: "+15551234567"
        displayOrder: 1
    LeadUpdate:
      type: object
      description: Request body for updating a lead row. All properties are optional.
      properties:
        fields:
          type: object
          additionalProperties: true
          description: >-
            Dynamic column cell values to merge onto the row, keyed by each column's
            nameId. Stored verbatim. Reserved keys are ignored.
        tabId:
          type: string
          nullable: true
          description: >-
            Tab to move the row to. A non-empty value must match a tab on the table;
            an empty value clears the tab.
        starred:
          type: boolean
          description: Whether the row is starred.
        displayOrder:
          type: number
          description: Manual sort position within the table. Must be a number.
      example:
        fields:
          email: jordan.rivera@newmail.com
        starred: true
        displayOrder: 2
    DeletedLead:
      type: object
      description: Confirmation object returned after soft-deleting a lead row.
      properties:
        id:
          type: string
          description: The id of the soft-deleted lead row.
        object:
          type: string
          description: The object type. Always "lead".
          example: lead
        deleted:
          type: boolean
          description: Always true on a successful delete.
          example: true
      required:
        - id
        - object
        - deleted
      example:
        id: lead_9fQ2x7TzKp
        object: lead
        deleted: true
    Audience:
      type: object
      description: >-
        A campaign audience - a saved, deduped recipient list a campaign sends to.
        The recipient contacts are paginated separately via the contacts
        sub-resource and are not included on this object. Tenant and bookkeeping
        fields (organizationId, recipients, sourceMeta, createdBy, deletedAt) are
        never exposed.
      properties:
        id:
          type: string
          description: Unique identifier for the audience. Use this as the audienceId in contact calls.
          example: 9KdxQwErTyUiOpAsDfGh
        object:
          type: string
          description: The object type. Always "audience".
          example: audience
        name:
          type: string
          description: Human-readable name of the audience.
          example: Website Signups
        source:
          type: string
          description: >-
            How the audience was built - "clients", "leads", or "upload" for the
            in-app builders, or "api" when provisioned via this API.
          example: api
        count:
          type: integer
          description: Number of contacts currently in the audience's send pool.
          example: 128
        createdAt:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 timestamp of when the audience was created.
          example: '2026-06-30T15:04:21.000Z'
        updatedAt:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 timestamp of when the audience was last updated.
          example: '2026-06-30T18:22:10.000Z'
      required:
        - id
        - object
        - name
        - source
        - count
      example:
        id: 9KdxQwErTyUiOpAsDfGh
        object: audience
        name: Website Signups
        source: api
        count: 128
        createdAt: '2026-06-30T15:04:21.000Z'
        updatedAt: '2026-06-30T18:22:10.000Z'

    AudienceCreate:
      type: object
      description: Request body for provisioning a new audience.
      properties:
        name:
          type: string
          description: A name for the audience (1-120 characters).
      required:
        - name
      example:
        name: Website Signups

    AudienceContact:
      type: object
      description: >-
        A single contact in an audience's send pool. The contact's identifier is its
        lowercased email - that is the cursor for pagination and the value used in
        the remove-contact path.
      properties:
        object:
          type: string
          description: The object type. Always "audience_contact".
          example: audience_contact
        audienceId:
          type: string
          description: The id of the audience this contact belongs to.
          example: 9KdxQwErTyUiOpAsDfGh
        email:
          type: string
          description: The contact's email address, lowercased.
          example: jordan.rivera@example.com
        firstName:
          type: string
          description: The contact's first name (empty string when unset).
          example: Jordan
        lastName:
          type: string
          description: The contact's last name (empty string when unset).
          example: Rivera
        contactId:
          type: string
          description: >-
            Source record id, present only when the contact was derived from a
            client or lead by an in-app builder. API-added contacts have none.
          example: usr_8f3a1c
        suppressed:
          type: boolean
          description: >-
            Whether the address is on the organization's suppression list. A
            suppressed contact stays listed but is skipped at send.
          example: false
      required:
        - object
        - audienceId
        - email
        - suppressed
      example:
        object: audience_contact
        audienceId: 9KdxQwErTyUiOpAsDfGh
        email: jordan.rivera@example.com
        firstName: Jordan
        lastName: Rivera
        suppressed: false

    AudienceContactInput:
      type: object
      description: Request body for adding or upserting a contact into an audience.
      properties:
        email:
          type: string
          description: The contact's email address (required). Matched case-insensitively.
        firstName:
          type: string
          description: Optional first name (max 80 characters).
        lastName:
          type: string
          description: Optional last name (max 80 characters).
      required:
        - email
      example:
        email: new.signup@example.com
        firstName: Jordan
        lastName: Rivera

    AudienceContactResult:
      type: object
      description: >-
        The result of adding or upserting a contact. `created` is true when the
        email was newly added, false when it already existed (name fields updated) or
        when it was suppressed. `suppressed` is true when the address is on the
        suppression list, in which case it is NOT added to the send pool.
      properties:
        object:
          type: string
          description: The object type. Always "audience_contact".
          example: audience_contact
        audienceId:
          type: string
          description: The id of the audience.
          example: 9KdxQwErTyUiOpAsDfGh
        email:
          type: string
          description: The contact's email address, lowercased.
          example: new.signup@example.com
        firstName:
          type: string
          description: The contact's first name as stored.
          example: Jordan
        lastName:
          type: string
          description: The contact's last name as stored.
          example: Rivera
        created:
          type: boolean
          description: True if the contact was newly added; false if it already existed or was suppressed.
          example: true
        added:
          type: boolean
          description: True if the contact was added to the send pool (false when suppressed).
          example: true
        suppressed:
          type: boolean
          description: True if the address is suppressed and was therefore not added to the send pool.
          example: false
        count:
          type: integer
          description: The audience's recipient count after the operation.
          example: 129
      required:
        - object
        - audienceId
        - email
        - created
        - added
        - suppressed
        - count
      example:
        object: audience_contact
        audienceId: 9KdxQwErTyUiOpAsDfGh
        email: new.signup@example.com
        firstName: Jordan
        lastName: Rivera
        created: true
        added: true
        suppressed: false
        count: 129

    DeletedAudienceContact:
      type: object
      description: Confirmation object returned after removing a contact from an audience.
      properties:
        object:
          type: string
          description: The object type. Always "audience_contact".
          example: audience_contact
        audienceId:
          type: string
          description: The id of the audience.
          example: 9KdxQwErTyUiOpAsDfGh
        email:
          type: string
          description: The email address that was targeted, lowercased.
          example: jordan.rivera@example.com
        deleted:
          type: boolean
          description: Always true on a successful call.
          example: true
        removed:
          type: boolean
          description: True if the contact was present and removed; false if it was not in the audience.
          example: true
        count:
          type: integer
          description: The audience's recipient count after the removal.
          example: 127
      required:
        - object
        - audienceId
        - email
        - deleted
        - removed
        - count
      example:
        object: audience_contact
        audienceId: 9KdxQwErTyUiOpAsDfGh
        email: jordan.rivera@example.com
        deleted: true
        removed: true
        count: 127
    EstimateLineItem:
      type: object
      description: A single line item on an estimate. Per-line amount is computed server-side as quantity * unitPrice.
      properties:
        entryId:
          type: string
          description: Stable identifier for the line row.
          example: row-1718600000000-a1b2c3d4
        itemId:
          type: string
          nullable: true
          description: Optional reference to a catalog item or service.
          example: null
        name:
          type: string
          example: Diagnostic visit
        description:
          type: string
          example: On-site inspection and diagnosis
        quantity:
          type: number
          example: 2
        unitPrice:
          type: number
          example: 75
        unitCost:
          type: number
          description: Optional internal cost per unit. Omitted when not set.
          example: 40
        taxable:
          type: boolean
          description: Whether the line is taxable. Defaults to true on create. Omitted from output when not set.
          example: true
        taxRate:
          description: >-
            Tax rate for the line. May be a fraction (0.1), a whole percent (10), or
            a display string ("10%"). Omitted from output when not set.
          oneOf:
            - type: number
            - type: string
          example: 0.1
        amount:
          type: number
          description: Computed line total (quantity * unitPrice).
          example: 150
        type:
          type: string
          description: Line type. Defaults to "item". Omitted from output when not set.
          example: item
      required:
        - name
        - quantity
        - unitPrice
        - amount

    EstimateClient:
      type: object
      description: >-
        Snapshot of the client this estimate was issued to, stored flat on the
        estimate document at create time. fullName is derived (read-only).
      properties:
        clientUUID:
          type: string
          nullable: true
          description: The client's short human code, if snapshotted.
          example: A7BCDE
        clientDocId:
          type: string
          nullable: true
          description: The client document id.
          example: 3aB7cD9eF1gH2iJ4kL6m
        firstName:
          type: string
          nullable: true
          example: Jane
        lastName:
          type: string
          nullable: true
          example: Doe
        fullName:
          type: string
          nullable: true
          description: Derived display name (firstName lastName, falling back to companyName). Read-only.
          example: Jane Doe
        companyName:
          type: string
          nullable: true
          example: Doe HVAC LLC
        email:
          type: string
          nullable: true
          example: jane@example.com
        phoneNumber:
          type: string
          nullable: true
          example: '+14155550123'
        address:
          type: string
          nullable: true
          description: Street address line.
          example: 123 Main St
        unit:
          type: string
          nullable: true
          example: Apt 4
        city:
          type: string
          nullable: true
          example: Austin
        state:
          type: string
          nullable: true
          example: TX
        zip:
          type: string
          nullable: true
          example: '78701'
        country:
          type: string
          nullable: true
          example: US

    EstimateBilledFrom:
      type: object
      description: Snapshot of the issuing company (billed-from) at the time the estimate was created.
      properties:
        companyName:
          type: string
          nullable: true
          example: Acme Field Services
        address:
          type: string
          nullable: true
          example: 500 Market St, San Francisco, CA
        email:
          type: string
          nullable: true
          example: billing@acme.example
        phone:
          type: string
          nullable: true
          example: '+14155559900'
        logoUrl:
          type: string
          nullable: true
          example: https://cdn.example.com/logo.png
        websiteUrl:
          type: string
          nullable: true
          example: https://acme.example
        timeZone:
          type: string
          nullable: true
          example: America/Los_Angeles
        timeZoneLabel:
          type: string
          nullable: true
          example: Pacific Time (US & Canada)

    EstimateSignature:
      type: object
      description: >-
        A signature on the estimate. Stored either as a signature document id
        (string) or an inline object; the serializer surfaces both forms.
      properties:
        id:
          type: string
          description: The signature document id.
          example: sig_8Hk2pLqR9sT0
        signerType:
          type: string
          nullable: true
          example: client
        signerName:
          type: string
          nullable: true
          example: Jane Doe
        signerEmail:
          type: string
          nullable: true
          example: jane@example.com
        signatureTitle:
          type: string
          nullable: true
          example: Owner
        signatureData:
          type: string
          nullable: true
          description: Base64-encoded signature image data.
        signedByUserId:
          type: string
          nullable: true
          example: 3aB7cD9eF1gH2iJ4kL6m
        signedAt:
          type: string
          format: date-time
          nullable: true
          example: '2026-06-17T14:45:00.000Z'

    Estimate:
      type: object
      description: A canonical estimate object - a complete mapping of the stored estimate document.
      properties:
        id:
          type: string
          description: The estimate document id.
          example: 8Hk2pLqR9sT0uVwXyZ12
        object:
          type: string
          example: estimate
        number:
          type: integer
          nullable: true
          description: Sequential human-facing estimate number allocated from organization numbering settings.
          example: 1042
        uuid:
          type: string
          nullable: true
          description: Short 6-char human code for the estimate.
          example: K7M2QR
        estimateName:
          type: string
          nullable: true
          description: A label for the estimate.
          example: Spring HVAC tune-up
        subject:
          type: string
          nullable: true
          description: Optional subject line.
          example: Estimate for spring maintenance
        clientId:
          type: string
          nullable: true
          description: The associated client id, if any.
          example: 3aB7cD9eF1gH2iJ4kL6m
        clientName:
          type: string
          nullable: true
          description: Display name derived from the estimate's snapshotted client fields.
          example: Jane Doe
        client:
          allOf:
            - $ref: '#/components/schemas/EstimateClient'
          nullable: true
          description: Full snapshot of the client. Omitted when no client fields were snapshotted.
        status:
          type: string
          enum:
            - draft
            - sent
            - accepted
            - declined
            - expired
            - void
          example: draft
        sent:
          type: boolean
          description: Whether the estimate has been marked as sent.
          example: false
        lineItems:
          type: array
          items:
            $ref: '#/components/schemas/EstimateLineItem'
        signatures:
          type: array
          description: Signatures on the estimate (ids or inline objects).
          items:
            $ref: '#/components/schemas/EstimateSignature'
        subtotal:
          type: number
          description: Server-computed subtotal. Alias of subTotal.
          example: 150
        subTotal:
          type: number
          description: Stored subtotal (alias of subtotal).
          example: 150
        taxTotal:
          type: number
          example: 15
        total:
          type: number
          description: Server-computed grand total. Alias of totalAmount.
          example: 165
        totalAmount:
          type: number
          description: Stored grand total (alias of total).
          example: 165
        discountTotal:
          type: number
          description: Total discount applied, if stored.
          example: 0
        tipTotal:
          type: number
          description: Total tip, if stored.
          example: 0
        taxableTotal:
          type: number
          description: Taxable portion of the subtotal, if stored.
          example: 150
        taxRate:
          description: Estimate-level tax rate, if stored. Fraction, percent, or display string.
          oneOf:
            - type: number
            - type: string
          example: 0.1
        itemCostTotal:
          type: number
          description: Total item cost, if stored.
          example: 80
        laborCostTotal:
          type: number
          description: Total labor cost, if stored.
          example: 0
        totalsSnapshot:
          type: object
          nullable: true
          additionalProperties: true
          description: A stored snapshot of computed totals, if present.
        currency:
          type: string
          example: USD
        terms:
          type: integer
          description: Payment terms in days.
          example: 30
        notes:
          type: string
          nullable: true
          example: Valid for 30 days.
        discountName:
          type: string
          nullable: true
          description: Label for the applied discount, if any.
          example: Spring promo
        organizationCompanyId:
          type: string
          nullable: true
          description: The issuing company (sub-org) id.
          example: cmp_8Hk2pLqR9sT0
        billedFrom:
          allOf:
            - $ref: '#/components/schemas/EstimateBilledFrom'
          nullable: true
          description: Snapshot of the issuing company. Omitted when not snapshotted.
        appointmentId:
          type: string
          nullable: true
          description: Linked appointment number, if any.
          example: '204'
        appointmentUUID:
          type: string
          nullable: true
          description: Linked appointment short code, if any.
          example: APT204
        appointmentDocId:
          type: string
          nullable: true
          description: Linked appointment document id, if any.
          example: 7Zx9yW8vU6tS5rQ4pO3n
        customFields:
          type: object
          additionalProperties: true
          description: Arbitrary organization-defined custom fields.
        convertedToInvoiceId:
          type: string
          nullable: true
          description: The invoice document id this estimate was converted into, if any.
          example: 9Lm3nQrS0tU1vWxYzA34
        convertedToInvoiceUuid:
          type: string
          nullable: true
          description: The short code of the invoice this estimate was converted into, if any.
          example: N4P8QR
        convertedAt:
          type: string
          format: date-time
          nullable: true
          description: When the estimate was converted to an invoice, if it was.
          example: '2026-06-17T15:00:00.000Z'
        issueDate:
          type: string
          format: date-time
          nullable: true
          description: The estimate creation timestamp.
          example: '2026-06-17T14:30:00.000Z'
        expiryDate:
          type: string
          format: date-time
          nullable: true
          description: When the estimate expires, if set. Alias of expiresAt.
          example: '2026-07-17T00:00:00.000Z'
        expiresAt:
          type: string
          format: date-time
          nullable: true
          description: When the estimate expires, if set (alias of expiryDate).
          example: '2026-07-17T00:00:00.000Z'
        reminderSentAt:
          type: string
          format: date-time
          nullable: true
          description: When a reminder was last sent for this estimate (read-only).
        createdAt:
          type: string
          format: date-time
          nullable: true
          example: '2026-06-17T14:30:00.000Z'
        updatedAt:
          type: string
          format: date-time
          nullable: true
          example: '2026-06-17T14:30:00.000Z'
      required:
        - id
        - object
        - status
        - lineItems
        - subtotal
        - taxTotal
        - total
        - currency
      example:
        id: 8Hk2pLqR9sT0uVwXyZ12
        object: estimate
        number: 1042
        uuid: K7M2QR
        estimateName: Spring HVAC tune-up
        subject: Estimate for spring maintenance
        clientId: 3aB7cD9eF1gH2iJ4kL6m
        clientName: Jane Doe
        client:
          clientDocId: 3aB7cD9eF1gH2iJ4kL6m
          firstName: Jane
          lastName: Doe
          fullName: Jane Doe
          email: jane@example.com
          phoneNumber: '+14155550123'
          address: 123 Main St
          city: Austin
          state: TX
          zip: '78701'
          country: US
        status: draft
        sent: false
        lineItems:
          - entryId: row-1718600000000-a1b2c3d4
            itemId: null
            name: Diagnostic visit
            description: On-site inspection and diagnosis
            quantity: 2
            unitPrice: 75
            taxable: true
            taxRate: 0.1
            amount: 150
            type: item
        signatures: []
        subtotal: 150
        subTotal: 150
        taxTotal: 15
        total: 165
        totalAmount: 165
        currency: USD
        terms: 30
        notes: Valid for 30 days.
        discountName: null
        organizationCompanyId: null
        appointmentId: null
        appointmentUUID: null
        appointmentDocId: null
        customFields: {}
        convertedToInvoiceId: null
        convertedToInvoiceUuid: null
        convertedAt: null
        issueDate: '2026-06-17T14:30:00.000Z'
        expiryDate: '2026-07-17T00:00:00.000Z'
        expiresAt: '2026-07-17T00:00:00.000Z'
        createdAt: '2026-06-17T14:30:00.000Z'
        updatedAt: '2026-06-17T14:30:00.000Z'

    EstimateClientInput:
      type: object
      description: >-
        Writable client snapshot. May also be supplied as top-level fields on the
        estimate body. fullName is derived and never written from the body.
      properties:
        clientUUID:
          type: string
          nullable: true
          example: A7BCDE
        firstName:
          type: string
          nullable: true
          example: Jane
        lastName:
          type: string
          nullable: true
          example: Doe
        companyName:
          type: string
          nullable: true
          example: Doe HVAC LLC
        email:
          type: string
          nullable: true
          example: jane@example.com
        phoneNumber:
          type: string
          nullable: true
          example: '+14155550123'
        address:
          type: string
          nullable: true
          example: 123 Main St
        unit:
          type: string
          nullable: true
          example: Apt 4
        city:
          type: string
          nullable: true
          example: Austin
        state:
          type: string
          nullable: true
          example: TX
        zip:
          type: string
          nullable: true
          example: '78701'
        country:
          type: string
          nullable: true
          example: US

    EstimateAppointmentInput:
      type: object
      description: >-
        Writable appointment linkage. May also be supplied as top-level fields on
        the estimate body.
      properties:
        appointmentId:
          type: string
          nullable: true
          example: '204'
        appointmentUUID:
          type: string
          nullable: true
          example: APT204
        appointmentDocId:
          type: string
          nullable: true
          example: 7Zx9yW8vU6tS5rQ4pO3n

    EstimateInput:
      type: object
      description: Writable fields shared by create and update of an estimate.
      properties:
        estimateName:
          type: string
          nullable: true
          description: A label for the estimate. Alias accepted as "title".
          example: Spring HVAC tune-up
        title:
          type: string
          description: Alias for estimateName.
          example: Spring HVAC tune-up
        subject:
          type: string
          nullable: true
          description: Optional subject line.
          example: Estimate for spring maintenance
        clientId:
          type: string
          nullable: true
          description: The client this estimate is for.
          example: 3aB7cD9eF1gH2iJ4kL6m
        client:
          $ref: '#/components/schemas/EstimateClientInput'
        appointment:
          $ref: '#/components/schemas/EstimateAppointmentInput'
        appointmentId:
          type: string
          nullable: true
          description: Linked appointment number (top-level alias of appointment.appointmentId).
          example: '204'
        appointmentUUID:
          type: string
          nullable: true
          description: Linked appointment short code (top-level alias).
          example: APT204
        appointmentDocId:
          type: string
          nullable: true
          description: Linked appointment document id (top-level alias).
          example: 7Zx9yW8vU6tS5rQ4pO3n
        organizationCompanyId:
          type: string
          nullable: true
          description: The issuing company (sub-org) id.
          example: cmp_8Hk2pLqR9sT0
        signatures:
          type: array
          description: Signature document ids (or {id} objects) to attach.
          items:
            oneOf:
              - type: string
              - type: object
                properties:
                  id:
                    type: string
        status:
          type: string
          enum:
            - draft
            - sent
            - accepted
            - declined
            - expired
            - void
          description: Estimate status. Defaults to "draft" on create.
          example: draft
        lineItems:
          type: array
          description: Line items. Per-line amount and the estimate totals are computed server-side.
          items:
            $ref: '#/components/schemas/EstimateLineItem'
        currency:
          type: string
          description: ISO currency code. Defaults to USD.
          example: USD
        notes:
          type: string
          nullable: true
          example: Valid for 30 days.
        discountName:
          type: string
          nullable: true
          description: Label for an applied discount.
          example: Spring promo
        expiryDate:
          type: string
          description: When the estimate expires. Stored as a string. Alias accepted as "expiresAt".
          example: '2026-07-17'
        expiresAt:
          type: string
          description: Alias for expiryDate.
          example: '2026-07-17'
        terms:
          type: integer
          description: Payment terms in days. Defaults to 0.
          example: 30
        customFields:
          type: object
          additionalProperties: true
          description: Arbitrary organization-defined custom fields.

    EstimateCreate:
      allOf:
        - $ref: '#/components/schemas/EstimateInput'
      example:
        estimateName: Spring HVAC tune-up
        clientId: 3aB7cD9eF1gH2iJ4kL6m
        status: draft
        currency: USD
        notes: Valid for 30 days.
        expiryDate: '2026-07-17'
        terms: 30
        lineItems:
          - name: Diagnostic visit
            description: On-site inspection and diagnosis
            quantity: 2
            unitPrice: 75
            taxable: true
            taxRate: 0.1

    EstimateUpdate:
      allOf:
        - $ref: '#/components/schemas/EstimateInput'
      example:
        status: sent
        notes: Updated terms - valid for 14 days.

    EstimateDeleted:
      type: object
      description: Confirmation returned when an estimate is soft-deleted.
      properties:
        id:
          type: string
          example: 8Hk2pLqR9sT0uVwXyZ12
        object:
          type: string
          example: estimate
        deleted:
          type: boolean
          example: true
      required:
        - id
        - object
        - deleted

    EstimateInvoice:
      type: object
      description: The invoice object returned by convert-to-invoice.
      properties:
        id:
          type: string
          example: 9Lm3nQrS0tU1vWxYzA34
        object:
          type: string
          example: invoice
        number:
          type: integer
          nullable: true
          example: 5021
        clientId:
          type: string
          nullable: true
          example: 3aB7cD9eF1gH2iJ4kL6m
        status:
          type: string
          example: draft
        lineItems:
          type: array
          items:
            $ref: '#/components/schemas/EstimateLineItem'
        subtotal:
          type: number
          example: 150
        taxTotal:
          type: number
          example: 15
        total:
          type: number
          example: 165
        balance:
          type: number
          example: 165
        amountPaid:
          type: number
          example: 0
        currency:
          type: string
          example: USD
        convertedFromEstimateId:
          type: string
          nullable: true
          example: 8Hk2pLqR9sT0uVwXyZ12
        createdAt:
          type: string
          format: date-time
          nullable: true
          example: '2026-06-17T15:00:00.000Z'
        updatedAt:
          type: string
          format: date-time
          nullable: true
          example: '2026-06-17T15:00:00.000Z'
      required:
        - id
        - object
        - status
        - lineItems
        - subtotal
        - taxTotal
        - total
        - balance
        - amountPaid
        - currency
    Task:
      type: object
      description: >-
        A task on the work-management board (environments -> groups -> tasks).
        environmentId is derived from the task's group and cannot be set directly.
        The fields map holds raw custom-field values with no schema validation.
      properties:
        id:
          type: string
          description: Unique identifier for the task.
        object:
          type: string
          description: The object type, always "task".
          example: task
        environmentId:
          type: string
          nullable: true
          description: The environment the task belongs to (derived from its group).
        groupId:
          type: string
          nullable: true
          description: The group the task belongs to.
        subGroupId:
          type: string
          nullable: true
          description: The sub-group within the group, or null when ungrouped.
        title:
          type: string
          description: The task title.
        description:
          type: string
          description: The task description.
        priority:
          type: string
          description: The task priority.
          enum:
            - lowest
            - low
            - normal
            - high
            - urgent
          example: normal
        assignees:
          type: array
          description: The members assigned to the task.
          items:
            $ref: '#/components/schemas/TaskAssignee'
        assigneeIds:
          type: array
          description: >-
            Read-only parallel list of assigned member ids (mirrors assignees).
            Derived server-side from assignees; not accepted on create or update.
          items:
            type: string
        tags:
          type: array
          description: Free-form tags on the task.
          items:
            type: string
        fields:
          type: object
          additionalProperties: true
          description: Raw custom-field values keyed by field id. Stored without validation.
        dueDate:
          type: string
          format: date-time
          nullable: true
          description: The due date and time as an ISO-8601 datetime, or null.
        endDate:
          type: string
          nullable: true
          description: The end date as a date-only string (YYYY-MM-DD), or null.
        position:
          type: number
          description: Sort position within the group.
          example: 0
        createdAt:
          type: string
          format: date-time
          nullable: true
          description: When the task was created (ISO-8601).
        updatedAt:
          type: string
          format: date-time
          nullable: true
          description: When the task was last updated (ISO-8601).
      example:
        id: tsk_8fK2mZ
        object: task
        environmentId: env_3aQ9
        groupId: grp_71Lp
        subGroupId: null
        title: Replace condenser fan motor
        description: Customer reports unit not cooling. Bring 1/3 HP motor.
        priority: high
        assignees:
          - id: usr_4Dx2
            name: Jordan Lee
            role: technician
        assigneeIds:
          - usr_4Dx2
        tags:
          - warranty
          - urgent-route
        fields:
          poNumber: PO-10482
        dueDate: '2026-06-20T15:00:00.000Z'
        endDate: '2026-06-20'
        position: 3
        createdAt: '2026-06-17T12:01:09.000Z'
        updatedAt: '2026-06-17T12:01:09.000Z'

    TaskAssignee:
      type: object
      description: A member assigned to a task.
      properties:
        id:
          type: string
          description: The member id.
        name:
          type: string
          description: The member's display name.
        role:
          type: string
          description: The member's role on the task.
      example:
        id: usr_4Dx2
        name: Jordan Lee
        role: technician

    TaskCreate:
      type: object
      required:
        - title
        - groupId
      description: >-
        Request body for creating a task. environmentId is derived from the group
        and cannot be supplied. subGroupId is honored only when it matches one of
        the group's sub-groups.
      properties:
        title:
          type: string
          description: The task title (required, non-empty).
        groupId:
          type: string
          description: The group this task belongs to (required, must be a live same-org group).
        description:
          type: string
          description: The task description.
        subGroupId:
          type: string
          nullable: true
          description: A sub-group id; ignored unless it matches one of the group's sub-groups.
        assignees:
          type: array
          description: Members to assign. Entries without an id are dropped and duplicates collapsed.
          items:
            $ref: '#/components/schemas/TaskAssignee'
        tags:
          type: array
          description: Free-form tags.
          items:
            type: string
        fields:
          type: object
          additionalProperties: true
          description: Raw custom-field values. Stored without validation.
        dueDate:
          type: string
          format: date-time
          nullable: true
          description: Due date and time; parsed to an ISO-8601 datetime or null.
        endDate:
          type: string
          nullable: true
          description: End date; normalized to a date-only string (YYYY-MM-DD) or null.
        priority:
          type: string
          enum:
            - lowest
            - low
            - normal
            - high
            - urgent
          description: Task priority; defaults to normal when omitted or invalid.
        position:
          type: number
          description: Sort position; auto-computed as max(existing in group)+1 when omitted.
      example:
        title: Replace condenser fan motor
        groupId: grp_71Lp
        description: Customer reports unit not cooling. Bring 1/3 HP motor.
        priority: high
        assignees:
          - id: usr_4Dx2
            name: Jordan Lee
            role: technician
        tags:
          - warranty
        dueDate: '2026-06-20T15:00:00.000Z'
        endDate: '2026-06-20'

    TaskUpdate:
      type: object
      description: >-
        Request body for updating a task. Only supplied fields are changed.
        Changing groupId re-derives environmentId and clears a stale subGroupId
        unless a new one is supplied.
      properties:
        title:
          type: string
          description: New title (cannot be empty).
        groupId:
          type: string
          description: Move the task to this group (must be a live same-org group).
        description:
          type: string
          description: New description.
        subGroupId:
          type: string
          nullable: true
          description: New sub-group id; empty or unknown clears it.
        assignees:
          type: array
          items:
            $ref: '#/components/schemas/TaskAssignee'
          description: Replacement assignees list.
        tags:
          type: array
          items:
            type: string
          description: Replacement tags list.
        fields:
          type: object
          additionalProperties: true
          description: Replacement raw custom-field values.
        dueDate:
          type: string
          format: date-time
          nullable: true
          description: New due date and time, or null to clear.
        endDate:
          type: string
          nullable: true
          description: New end date (date-only), or null to clear.
        priority:
          type: string
          enum:
            - lowest
            - low
            - normal
            - high
            - urgent
          description: New priority; falls back to normal when invalid.
        position:
          type: number
          description: New sort position.
      example:
        priority: urgent
        assignees:
          - id: usr_4Dx2
            name: Jordan Lee
            role: technician

    TaskDeleted:
      type: object
      description: Confirmation returned when a task is soft-deleted.
      properties:
        id:
          type: string
          description: The id of the deleted task.
        object:
          type: string
          example: task
        deleted:
          type: boolean
          example: true
      example:
        id: tsk_8fK2mZ
        object: task
        deleted: true
    TaskEnvironmentAssignee:
      type: object
      description: A single assignee on a task environment.
      properties:
        id:
          type: string
          description: The member ID.
        name:
          type: string
          description: The member display name.
        role:
          type: string
          description: The member role. Omitted when empty.
      example:
        id: usr_8sd9f2k
        name: Dana Lee
        role: manager

    TaskSubGroup:
      type: object
      description: A nested subgroup on a task group.
      properties:
        id:
          type: string
        name:
          type: string
        color:
          type: string
          description: Hex or named color. Omitted when empty.
        position:
          type: number
      example:
        id: sub_29fk2
        name: Backlog
        color: '#7C3AED'
        position: 0

    TaskGroupCreate:
      type: object
      description: Request body for creating a task group inside an environment.
      properties:
        name:
          type: string
          description: The group's name (required).
        color:
          type: string
          description: Optional hex/named color swatch for the group.
        position:
          type: integer
          description: Optional ordering position; defaults to the end of the environment.
      required:
        - name
      example:
        name: To do
        color: '#f59e0b'

    TaskGroup:
      type: object
      description: >-
        A task group within an environment. Returned on the environment retrieve
        (GET by id) expansion, the dedicated GET /task-environments/{id}/groups
        list, and the POST /task-environments/{id}/groups create. Use the group id
        as the groupId when creating a task.
      properties:
        id:
          type: string
        object:
          type: string
          example: task_group
        environmentId:
          type: string
          description: The parent environment this group belongs to.
        name:
          type: string
        color:
          type: string
          description: Hex or named color. Omitted when empty.
        position:
          type: number
          description: Ordering position within the environment; also the sort key.
        isDefault:
          type: boolean
        collapsed:
          type: boolean
          description: UI collapse state. Omitted when the underlying doc has never set it.
        subGroups:
          type: array
          items:
            $ref: '#/components/schemas/TaskSubGroup'
        createdByName:
          type: string
          nullable: true
          description: Display name of the member who created the group. null on auto-seeded default groups.
        createdAt:
          type: string
          format: date-time
          nullable: true
        updatedAt:
          type: string
          format: date-time
          nullable: true
      example:
        id: grp_19sk2
        object: task_group
        environmentId: env_5k29df
        name: In Progress
        color: '#2563EB'
        position: 1
        isDefault: false
        collapsed: false
        subGroups: []
        createdByName: Dana Lee
        createdAt: '2026-06-12T18:04:11.000Z'
        updatedAt: '2026-06-12T18:04:11.000Z'

    TaskEnvironment:
      type: object
      description: A task environment - the top level of the work-management board (environments -> groups -> tasks).
      properties:
        id:
          type: string
        object:
          type: string
          example: task_environment
        name:
          type: string
        icon:
          type: string
          description: Material symbol icon name. Omitted when absent on the underlying doc.
        color:
          type: string
          description: Color. Normally absent on environment docs (only groups carry color); surfaced if set.
        isDefault:
          type: boolean
        position:
          type: number
        assignees:
          type: array
          items:
            $ref: '#/components/schemas/TaskEnvironmentAssignee'
        assigneeIds:
          type: array
          items:
            type: string
          description: Server-derived parallel array of assignee IDs.
        createdByName:
          type: string
          nullable: true
          description: Display name of the member who created the environment. null on the auto-seeded default environment.
        groups:
          type: array
          description: Expanded groups. Present only on the retrieve (GET by id) endpoint; omitted from the list endpoint.
          items:
            $ref: '#/components/schemas/TaskGroup'
        createdAt:
          type: string
          format: date-time
          nullable: true
        updatedAt:
          type: string
          format: date-time
          nullable: true
      example:
        id: env_5k29df
        object: task_environment
        name: Client Onboarding
        icon: folder
        isDefault: false
        position: 2
        assignees:
          - id: usr_8sd9f2k
            name: Dana Lee
            role: manager
        assigneeIds:
          - usr_8sd9f2k
        createdByName: Dana Lee
        createdAt: '2026-06-12T18:04:11.000Z'
        updatedAt: '2026-06-14T09:22:01.000Z'

    TaskEnvironmentCreate:
      type: object
      required:
        - name
      description: Request body to create a task environment.
      properties:
        name:
          type: string
          description: The environment name. Required, non-empty.
        icon:
          type: string
          description: Material symbol icon name. Defaults to "folder" when omitted or empty.
        position:
          type: number
          description: Ordering position. Server-derived as max(existing)+1 when omitted.
        assignees:
          type: array
          description: Assignee entries. Each must carry an id (id/Id/uuid); entries without an id are dropped, duplicates by id are deduped.
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              role:
                type: string
      example:
        name: Client Onboarding
        icon: folder
        assignees:
          - id: usr_8sd9f2k
            name: Dana Lee
            role: manager

    TaskEnvironmentUpdate:
      type: object
      description: Request body to update a task environment. Only provided fields are merged.
      properties:
        name:
          type: string
          description: The environment name. Cannot be set to empty.
        icon:
          type: string
          description: Material symbol icon name. Defaults to "folder" when set to empty.
        position:
          type: number
          description: Ordering position. Falls back to 0 if not a finite number.
        assignees:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              role:
                type: string
      example:
        name: Client Onboarding (Q3)
        position: 3

    TaskEnvironmentDeleted:
      type: object
      description: Confirmation returned after soft-deleting a task environment.
      properties:
        id:
          type: string
        object:
          type: string
          example: task_environment
        deleted:
          type: boolean
        groupsRemoved:
          type: integer
          description: Number of active groups soft-deleted in the cascade.
        tasksRemoved:
          type: integer
          description: Number of active tasks soft-deleted in the cascade.
      example:
        id: env_5k29df
        object: task_environment
        deleted: true
        groupsRemoved: 3
        tasksRemoved: 14
    Reminder:
      type: object
      description: >-
        A per-user reminder. Reminders are owned by a single member - the API key
        only ever returns the reminders created by its own issuing member. Internal
        fields (organizationId, ownerUserId, notifiedAt, deletedAt) are stripped
        from the public object.
      properties:
        id:
          type: string
          description: Unique identifier for the reminder.
        object:
          type: string
          description: The object type. Always "reminder".
          example: reminder
        title:
          type: string
          description: The reminder text. Trimmed and capped at 500 characters.
        remindAt:
          type: string
          format: date-time
          nullable: true
          description: >-
            ISO-8601 time the reminder should fire. Null means "Save for Later"
            (no scheduled notification).
        status:
          type: string
          description: The reminder status.
          enum:
            - inProgress
            - resolved
          example: inProgress
        resolvedAt:
          type: string
          format: date-time
          nullable: true
          description: >-
            ISO-8601 time the reminder was resolved. Null while the reminder is in
            progress.
        createdAt:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 creation timestamp.
        updatedAt:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 last-update timestamp.
      required:
        - id
        - object
        - title
        - status
      example:
        id: rmd_8f2c1a9b4e
        object: reminder
        title: Follow up with the Acme account before noon
        remindAt: '2026-06-18T16:00:00.000Z'
        status: inProgress
        resolvedAt: null
        createdAt: '2026-06-17T14:32:00.000Z'
        updatedAt: '2026-06-17T14:32:00.000Z'
    ReminderCreate:
      type: object
      description: Request body for creating a reminder.
      properties:
        title:
          type: string
          description: >-
            The reminder text. Required. Trimmed and capped at 500 characters.
        remindAt:
          type: string
          format: date-time
          nullable: true
          description: >-
            ISO-8601 time the reminder should fire. Omit, send null, or send an
            empty string for "Save for Later". An unparseable value is silently
            coerced to null.
      required:
        - title
      example:
        title: Follow up with the Acme account before noon
        remindAt: '2026-06-18T16:00:00.000Z'
    ReminderUpdate:
      type: object
      description: >-
        Request body for updating a reminder. Each field is applied only when
        present. At least one field should be supplied.
      properties:
        title:
          type: string
          description: >-
            The reminder text. When present it must be non-empty after trimming.
            Capped at 500 characters.
        remindAt:
          type: string
          format: date-time
          nullable: true
          description: >-
            ISO-8601 time the reminder should fire. Setting this re-arms the
            notification. Null or empty clears the schedule. An unparseable value is
            silently coerced to null.
        status:
          type: string
          description: >-
            The reminder status. Setting "resolved" stamps resolvedAt; setting
            "inProgress" clears it.
          enum:
            - inProgress
            - resolved
      example:
        title: Follow up with the Acme account this afternoon
        status: resolved
    DeletedReminder:
      type: object
      description: Confirmation returned when a reminder is soft-deleted.
      properties:
        id:
          type: string
          description: The identifier of the deleted reminder.
        object:
          type: string
          description: The object type. Always "reminder".
          example: reminder
        deleted:
          type: boolean
          description: Always true.
          example: true
      required:
        - id
        - object
        - deleted
      example:
        id: rmd_8f2c1a9b4e
        object: reminder
        deleted: true
    Invoice:
      type: object
      description: >-
        A canonical invoice object - a complete mapping of the stored invoice
        document. Money totals (subtotal, taxTotal, total, balance, and the derived
        discount/tip/cost totals) are always computed server-side from the line
        items and recorded payments; they are never trusted from the request body.
        The client snapshot, linkage, and company fields reflect what was stamped on
        the invoice at edit time.
      properties:
        id:
          type: string
          description: Unique invoice identifier (the Firestore document id).
        object:
          type: string
          description: The object type. Always "invoice".
          example: invoice
        number:
          type: integer
          nullable: true
          description: >-
            The sequential numeric business number assigned from the organization's
            numbering settings. Null if numbering could not be assigned.
        uuid:
          type: string
          nullable: true
          description: Short human-friendly reference code (e.g. for portal links).
          example: K7P2QM
        invoiceName:
          type: string
          nullable: true
          description: Optional display name/title for the invoice.
        status:
          type: string
          description: The invoice status (e.g. draft, sent, paid).
          example: draft
        currency:
          type: string
          description: ISO currency code.
          example: USD
        terms:
          type: integer
          description: Payment terms in days (net days). Defaults to 0.
        dueDate:
          type: string
          nullable: true
          description: Local due date in YYYY-MM-DD form.
          example: '2026-07-01'
        sent:
          type: boolean
          description: Whether the invoice has been marked as sent.
        sentAt:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 timestamp the invoice was sent, if ever.
        subtotal:
          type: number
          description: Sum of all line-item amounts (quantity * unitPrice), computed server-side.
        taxTotal:
          type: number
          description: Total tax across taxable line items, computed server-side.
        total:
          type: number
          description: subtotal plus taxTotal, computed server-side.
        discountTotal:
          type: number
          description: Total discount applied, computed server-side.
        discountName:
          type: string
          nullable: true
          description: Optional label for the applied discount.
        tipTotal:
          type: number
          description: Total tip applied, computed server-side.
        taxableTotal:
          type: number
          description: Sum of taxable line-item amounts, computed server-side.
        taxRate:
          type: string
          nullable: true
          description: Invoice-level tax rate, when stored as a single display value.
        itemCostTotal:
          type: number
          description: Total item cost across line items, computed server-side.
        laborCostTotal:
          type: number
          description: Total labor cost across line items, computed server-side.
        amountPaid:
          type: number
          description: Total amount paid against this invoice.
        balance:
          type: number
          description: Outstanding balance (total minus amountPaid).
        notes:
          type: string
          nullable: true
          description: Free-form notes on the invoice.
        clientId:
          type: string
          nullable: true
          description: Identifier of the client this invoice belongs to.
        clientUUID:
          type: string
          nullable: true
          description: Short reference code of the linked client.
        clientDocId:
          type: string
          nullable: true
          description: Firestore document id of the linked client.
        clientName:
          type: string
          nullable: true
          description: Display name of the client.
        firstName:
          type: string
          nullable: true
          description: Client first name snapshot.
        lastName:
          type: string
          nullable: true
          description: Client last name snapshot.
        companyName:
          type: string
          nullable: true
          description: Client company name snapshot.
        email:
          type: string
          nullable: true
          description: Client email snapshot.
        phoneNumber:
          type: string
          nullable: true
          description: Client phone number snapshot.
        address:
          $ref: '#/components/schemas/InvoiceAddress'
        appointmentId:
          type: string
          nullable: true
          description: Business number of a linked appointment, if any.
        appointmentUUID:
          type: string
          nullable: true
          description: Short reference code of a linked appointment.
        appointmentDocId:
          type: string
          nullable: true
          description: Firestore document id of a linked appointment.
        organizationCompanyId:
          type: string
          nullable: true
          description: Identifier of the issuing organization company.
        billedFrom:
          $ref: '#/components/schemas/InvoiceBilledFrom'
        lineItems:
          type: array
          description: The invoice line items.
          items:
            $ref: '#/components/schemas/InvoiceLineItem'
        signatures:
          type: array
          description: Signature references attached to the invoice.
          items:
            type: object
            additionalProperties: true
        signature:
          type: object
          nullable: true
          additionalProperties: true
          description: Legacy single-signature object, when present.
        customFields:
          type: object
          additionalProperties: true
          description: Arbitrary organization-defined custom field values.
        issueDate:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 timestamp the invoice was created (mirrors createdAt).
        reminderSentAt:
          type: string
          format: date-time
          nullable: true
          description: When a payment reminder was last sent for this invoice (read-only).
        createdAt:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 creation timestamp.
        updatedAt:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 last-update timestamp.
      example:
        id: 7sKq2dM3vPq1aBcDeFgH
        object: invoice
        number: 1042
        uuid: K7P2QM
        invoiceName: Spring HVAC service
        status: sent
        currency: USD
        terms: 30
        dueDate: '2026-07-17'
        sent: true
        sentAt: '2026-06-17T14:25:00.000Z'
        subtotal: 150
        taxTotal: 15
        total: 165
        discountTotal: 0
        discountName: null
        tipTotal: 0
        taxableTotal: 150
        taxRate: null
        itemCostTotal: 0
        laborCostTotal: 0
        amountPaid: 0
        balance: 165
        notes: Net 30 terms apply.
        clientId: cli_9aZ8xY7w
        clientUUID: CL9A8X
        clientDocId: cli_9aZ8xY7w
        clientName: Acme Clinic
        firstName: Jane
        lastName: Doe
        companyName: Acme Clinic
        email: jane@acme.example
        phoneNumber: '+15551234567'
        address:
          line1: 123 Main St
          unit: '4B'
          city: Austin
          state: TX
          zip: '78701'
          country: US
        appointmentId: '512'
        appointmentUUID: AP512X
        appointmentDocId: apt_4dF8kL
        organizationCompanyId: comp_main
        billedFrom:
          companyName: Genaya HVAC
          address: 9 Industrial Way, Austin TX
          email: billing@genaya-hvac.example
          phone: '+15559876543'
          logoUrl: https://cdn.example/logo.png
          websiteUrl: https://genaya-hvac.example
          timeZone: America/Chicago
          timeZoneLabel: Central Time
        lineItems:
          - entryId: row-1718634000000-0
            itemId: itm_tuneup
            itemandservicesId: itm_tuneup
            name: HVAC tune-up
            description: Seasonal system inspection
            quantity: 1
            unitPrice: 150
            unitCost: 80
            amount: 150
            taxable: true
            taxRate: 0.1
            type: item
        signatures: []
        signature: null
        customFields: {}
        issueDate: '2026-06-17T14:22:05.000Z'
        createdAt: '2026-06-17T14:22:05.000Z'
        updatedAt: '2026-06-17T14:22:05.000Z'

    InvoiceAddress:
      type: object
      nullable: true
      description: The client address snapshot stamped on the invoice.
      properties:
        line1:
          type: string
          nullable: true
          description: Street address line.
        unit:
          type: string
          nullable: true
          description: Unit, suite, or apartment.
        city:
          type: string
          nullable: true
        state:
          type: string
          nullable: true
        zip:
          type: string
          nullable: true
        country:
          type: string
          nullable: true

    InvoiceBilledFrom:
      type: object
      nullable: true
      description: >-
        Read-only snapshot of the issuing organization company at the time the
        invoice was created or its company was changed. Never accepted on input.
      properties:
        companyName:
          type: string
          nullable: true
        address:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
        phone:
          type: string
          nullable: true
        logoUrl:
          type: string
          nullable: true
        websiteUrl:
          type: string
          nullable: true
        timeZone:
          type: string
          nullable: true
        timeZoneLabel:
          type: string
          nullable: true

    InvoiceLineItem:
      type: object
      description: A single line item on an invoice.
      properties:
        entryId:
          type: string
          nullable: true
          description: Stable per-row identifier.
        itemId:
          type: string
          nullable: true
          description: Catalog item/service reference, if the line came from the catalog.
        itemandservicesId:
          type: string
          nullable: true
          description: >-
            Catalog reference used by inventory fulfillment. Present on invoices
            authored in-app; null for API-created lines.
        name:
          type: string
          nullable: true
          description: The line item name.
        description:
          type: string
          nullable: true
          description: Optional line item description.
        quantity:
          type: number
          description: Quantity of the item.
        unitPrice:
          type: number
          description: Price per unit.
        unitCost:
          type: number
          description: Cost per unit (for margin tracking).
        amount:
          type: number
          description: Line total (quantity * unitPrice).
        taxable:
          type: boolean
          description: Whether tax applies to this line.
        taxRate:
          type: number
          description: Tax rate as a fraction in the range 0 to 1 (e.g. 0.1 for 10 percent).
        type:
          type: string
          nullable: true
          description: Line item type (e.g. item, custom, labor).

    InvoiceCreate:
      type: object
      description: Request body for creating an invoice.
      required:
        - lineItems
      properties:
        lineItems:
          type: array
          description: The invoice line items. At least one is required.
          minItems: 1
          items:
            $ref: '#/components/schemas/InvoiceLineItemInput'
        invoiceName:
          type: string
          description: Optional display name/title for the invoice.
        status:
          type: string
          description: Initial status. Defaults to "draft".
          example: draft
        dueDate:
          type: string
          description: Due date in YYYY-MM-DD form. Defaults to today.
          example: '2026-07-17'
        terms:
          type: integer
          description: Payment terms in days (net days). Defaults to 0.
        currency:
          type: string
          description: ISO currency code. Defaults to USD.
          example: USD
        notes:
          type: string
          description: Free-form notes.
        discountName:
          type: string
          description: Optional label for an applied discount.
        clientId:
          type: string
          description: Identifier of the client this invoice belongs to.
        clientUUID:
          type: string
          description: Short reference code of the linked client.
        clientDocId:
          type: string
          description: Firestore document id of the linked client.
        clientName:
          type: string
          description: Display name of the client.
        firstName:
          type: string
          description: Client first name snapshot.
        lastName:
          type: string
          description: Client last name snapshot.
        companyName:
          type: string
          description: Client company name snapshot.
        email:
          type: string
          description: Client email snapshot.
        phoneNumber:
          type: string
          description: Client phone number snapshot.
        address:
          $ref: '#/components/schemas/InvoiceAddress'
        appointmentId:
          type: string
          description: Business number of a linked appointment.
        appointmentUUID:
          type: string
          description: Short reference code of a linked appointment.
        appointmentDocId:
          type: string
          description: Firestore document id of a linked appointment.
        organizationCompanyId:
          type: string
          description: Identifier of the issuing organization company.
        customFields:
          type: object
          description: Arbitrary organization-defined custom field values.
          additionalProperties: true
      example:
        clientId: cli_9aZ8xY7w
        clientName: Acme Clinic
        invoiceName: Spring HVAC service
        status: draft
        currency: USD
        terms: 30
        dueDate: '2026-07-17'
        notes: Net 30 terms apply.
        lineItems:
          - name: HVAC tune-up
            description: Seasonal system inspection
            quantity: 1
            unitPrice: 150
            unitCost: 80
            taxable: true
            taxRate: '10%'
            type: item

    InvoiceUpdate:
      type: object
      description: >-
        Request body for updating an invoice. Only supplied fields are changed.
        Supplying lineItems triggers a server-side recompute of subtotal, taxTotal,
        total, and balance. Money totals and the billedFrom snapshot are never
        accepted from the body.
      properties:
        lineItems:
          type: array
          description: Replacement line items. At least one is required when supplied.
          minItems: 1
          items:
            $ref: '#/components/schemas/InvoiceLineItemInput'
        invoiceName:
          type: string
          nullable: true
          description: Optional display name/title for the invoice.
        status:
          type: string
          description: The invoice status.
        dueDate:
          type: string
          nullable: true
          description: Due date in YYYY-MM-DD form.
        terms:
          type: integer
          description: Payment terms in days (net days).
        currency:
          type: string
          description: ISO currency code.
        notes:
          type: string
          nullable: true
          description: Free-form notes.
        discountName:
          type: string
          nullable: true
          description: Optional label for an applied discount.
        sent:
          type: boolean
          description: Whether the invoice is marked as sent.
        clientId:
          type: string
          nullable: true
          description: Identifier of the client this invoice belongs to.
        clientUUID:
          type: string
          nullable: true
          description: Short reference code of the linked client.
        clientDocId:
          type: string
          nullable: true
          description: Firestore document id of the linked client.
        clientName:
          type: string
          nullable: true
          description: Display name of the client.
        firstName:
          type: string
          nullable: true
          description: Client first name snapshot.
        lastName:
          type: string
          nullable: true
          description: Client last name snapshot.
        companyName:
          type: string
          nullable: true
          description: Client company name snapshot.
        email:
          type: string
          nullable: true
          description: Client email snapshot.
        phoneNumber:
          type: string
          nullable: true
          description: Client phone number snapshot.
        address:
          $ref: '#/components/schemas/InvoiceAddress'
        appointmentId:
          type: string
          nullable: true
          description: Business number of a linked appointment.
        appointmentUUID:
          type: string
          nullable: true
          description: Short reference code of a linked appointment.
        appointmentDocId:
          type: string
          nullable: true
          description: Firestore document id of a linked appointment.
        organizationCompanyId:
          type: string
          nullable: true
          description: Identifier of the issuing organization company.
        customFields:
          type: object
          description: Arbitrary organization-defined custom field values.
          additionalProperties: true
      example:
        status: sent
        notes: Updated terms - due in 15 days.

    InvoiceLineItemInput:
      type: object
      description: A line item supplied when creating or updating an invoice.
      required:
        - name
        - quantity
        - unitPrice
      properties:
        name:
          type: string
          description: The line item name.
        description:
          type: string
          description: Optional line item description.
        quantity:
          type: number
          description: Quantity of the item. Must be greater than 0.
        unitPrice:
          type: number
          description: Price per unit. Must be 0 or greater.
        unitCost:
          type: number
          description: Cost per unit. Must be 0 or greater. Defaults to 0.
        taxable:
          type: boolean
          description: Whether tax applies to this line. Defaults to true.
        taxRate:
          oneOf:
            - type: number
            - type: string
          description: >-
            Tax rate accepted as a fraction (0..1), a percent number (0..100), or a
            percent string such as "10%". Normalized to a fraction server-side.
        type:
          type: string
          description: Line item type (e.g. item, custom, labor). Defaults to "item".
        itemId:
          type: string
          description: Optional catalog item/service reference.

    InvoiceDeleted:
      type: object
      description: Confirmation returned when an invoice is soft-deleted.
      properties:
        id:
          type: string
          description: The deleted invoice id.
        object:
          type: string
          example: invoice
        deleted:
          type: boolean
          example: true
      example:
        id: 7sKq2dM3vPq1aBcDeFgH
        object: invoice
        deleted: true
    Expense:
      type: object
      description: An expense record, scoped to the authenticated organization.
      properties:
        id:
          type: string
          description: Unique identifier for the expense.
        object:
          type: string
          description: The object type. Always "expense".
          example: expense
        vendorName:
          type: string
          description: The name of the vendor the expense was paid to.
        date:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 timestamp of the expense date.
        referenceNo:
          type: string
          description: A reference or invoice number for the expense.
        paymentMethod:
          type: string
          description: The payment method used (free-form text).
        total:
          type: number
          description: The total amount of the expense. Defaults to 0.
        tax:
          type: number
          description: The tax portion of the expense. Defaults to 0.
        currency:
          type: string
          description: ISO currency code. Defaults to "USD".
        appointmentId:
          type: string
          nullable: true
          description: The id of an associated appointment, or null if none.
        appointmentUuid:
          type: string
          nullable: true
          description: The stable UUID of the associated appointment, or null if none.
        appointmentDocId:
          type: string
          nullable: true
          description: The Firestore document id of the associated appointment, or null if none.
        reimburseToId:
          type: string
          nullable: true
          description: The id of the team member or external company to reimburse, or null.
        reimburseToName:
          type: string
          nullable: true
          description: The display name of the reimbursement recipient, or null.
        organizationCompanyId:
          type: string
          nullable: true
          description: The id of the organization company this expense is attributed to, or null.
        organizationCompanyName:
          type: string
          nullable: true
          description: The name of the organization company, or null.
        organizationCompanyLogoUrl:
          type: string
          nullable: true
          description: The logo URL of the organization company, or null.
        items:
          type: array
          description: The line items that make up the expense.
          items:
            $ref: '#/components/schemas/ExpenseItem'
        status:
          type: string
          description: The expense status. Defaults to "created".
          example: created
        createdAt:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 timestamp of when the expense was created.
        updatedAt:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 timestamp of when the expense was last updated.
      example:
        id: exp_8f2c1a9b
        object: expense
        vendorName: Home Depot
        date: '2026-06-15T14:30:00.000Z'
        referenceNo: INV-4821
        paymentMethod: card
        total: 248.75
        tax: 18.5
        currency: USD
        appointmentId: appt_5a9d22c4
        appointmentUuid: 1c8f2a3b-4d5e-6f70-8192-a3b4c5d6e7f8
        appointmentDocId: 7Qx2mF0aBcDeFgHiJkLm
        reimburseToId: usr_3b9e21a7
        reimburseToName: Jordan Avery
        organizationCompanyId: cmp_1a2b3c4d
        organizationCompanyName: Northside HVAC
        organizationCompanyLogoUrl: https://storage.genaya.com/logos/northside.png
        items:
          - id: itm_7c0d3e1f
            name: PVC pipe
            description: 2 inch schedule 40
            amount: 42.5
            categoryId: cat_materials
            appointmentId: null
        status: created
        createdAt: '2026-06-15T14:31:02.000Z'
        updatedAt: '2026-06-15T14:31:02.000Z'
    ExpenseItem:
      type: object
      description: A single line item on an expense.
      properties:
        id:
          type: string
          nullable: true
          description: The id of the line item, or null when not set.
        name:
          type: string
          description: The line item name.
        description:
          type: string
          description: A free-form description of the line item.
        amount:
          type: number
          description: The amount for this line item.
        categoryId:
          type: string
          nullable: true
          description: The category id this line item belongs to, or null.
        appointmentId:
          type: string
          nullable: true
          description: An appointment id associated with this line item, or null.
    ExpenseCreate:
      type: object
      description: |
        Payload to create an expense. Provide a vendorName or at least one item.
      properties:
        vendorName:
          type: string
          description: The name of the vendor.
        date:
          type: string
          format: date-time
          description: ISO-8601 expense date. Defaults to now when omitted.
        referenceNo:
          type: string
          description: A reference or invoice number.
        paymentMethod:
          type: string
          description: The payment method used.
        total:
          type: number
          description: The total amount. Defaults to 0.
        tax:
          type: number
          description: The tax portion. Defaults to 0.
        currency:
          type: string
          description: ISO currency code. Defaults to "USD".
        appointmentId:
          type: string
          nullable: true
          description: The id of an associated appointment.
        appointmentUuid:
          type: string
          nullable: true
          description: The stable UUID of the associated appointment.
        appointmentDocId:
          type: string
          nullable: true
          description: The Firestore document id of the associated appointment.
        reimburseToId:
          type: string
          nullable: true
          description: The id of the team member or external company to reimburse.
        reimburseToName:
          type: string
          nullable: true
          description: The display name of the reimbursement recipient.
        organizationCompanyId:
          type: string
          nullable: true
          description: The id of the organization company this expense is attributed to.
        organizationCompanyName:
          type: string
          nullable: true
          description: The name of the organization company.
        organizationCompanyLogoUrl:
          type: string
          nullable: true
          description: The logo URL of the organization company.
        items:
          type: array
          description: The line items for the expense.
          items:
            $ref: '#/components/schemas/ExpenseItemInput'
      example:
        vendorName: Home Depot
        date: '2026-06-15T14:30:00.000Z'
        referenceNo: INV-4821
        paymentMethod: card
        total: 248.75
        tax: 18.5
        currency: USD
        appointmentId: appt_5a9d22c4
        reimburseToId: usr_3b9e21a7
        reimburseToName: Jordan Avery
        organizationCompanyId: cmp_1a2b3c4d
        organizationCompanyName: Northside HVAC
        items:
          - name: PVC pipe
            description: 2 inch schedule 40
            amount: 42.5
            categoryId: cat_materials
    ExpenseUpdate:
      type: object
      description: |
        Payload to update an expense. Only the fields you supply are changed. The
        resulting record must still have a vendorName or at least one item.
      properties:
        vendorName:
          type: string
          description: The name of the vendor.
        date:
          type: string
          format: date-time
          description: ISO-8601 expense date.
        referenceNo:
          type: string
          description: A reference or invoice number.
        paymentMethod:
          type: string
          description: The payment method used.
        total:
          type: number
          description: The total amount.
        tax:
          type: number
          description: The tax portion.
        currency:
          type: string
          description: ISO currency code.
        appointmentId:
          type: string
          nullable: true
          description: The id of an associated appointment.
        appointmentUuid:
          type: string
          nullable: true
          description: The stable UUID of the associated appointment.
        appointmentDocId:
          type: string
          nullable: true
          description: The Firestore document id of the associated appointment.
        reimburseToId:
          type: string
          nullable: true
          description: The id of the team member or external company to reimburse.
        reimburseToName:
          type: string
          nullable: true
          description: The display name of the reimbursement recipient.
        organizationCompanyId:
          type: string
          nullable: true
          description: The id of the organization company this expense is attributed to.
        organizationCompanyName:
          type: string
          nullable: true
          description: The name of the organization company.
        organizationCompanyLogoUrl:
          type: string
          nullable: true
          description: The logo URL of the organization company.
        items:
          type: array
          description: The line items for the expense (replaces the existing array).
          items:
            $ref: '#/components/schemas/ExpenseItemInput'
      example:
        total: 260.0
        paymentMethod: cash
    ExpenseItemInput:
      type: object
      description: A line item supplied when creating or updating an expense.
      properties:
        id:
          type: string
          description: Optional client-supplied id; generated when omitted.
        name:
          type: string
          description: The line item name.
        description:
          type: string
          description: A free-form description.
        amount:
          type: number
          description: The amount for this line item. Defaults to 0.
        categoryId:
          type: string
          description: The category id this line item belongs to.
        appointmentId:
          type: string
          nullable: true
          description: An appointment id associated with this line item.
    DeletedExpense:
      type: object
      description: Confirmation returned when an expense is soft-deleted.
      properties:
        id:
          type: string
          description: The id of the deleted expense.
        object:
          type: string
          example: expense
        deleted:
          type: boolean
          example: true
    TeamMember:
      type: object
      description: A member of the organization's team (roster). Read-only in v1.
      properties:
        id:
          type: string
          description: Unique identifier of the team member.
        object:
          type: string
          example: team_member
        name:
          type: string
          description: The member's display name.
        email:
          type: string
          description: The member's email address.
        phone:
          type: string
          description: The member's phone number, when set.
        role:
          type: string
          description: The member's role / permission level.
          enum:
            - admin
            - manager
            - dispatcher
            - employee
            - externalEmployee
            - owner
        status:
          type: string
          description: Membership status (e.g. active, inactive).
          example: active
        emailVerified:
          type: boolean
          description: Whether the member has verified their email / accepted their invite.
        createdAt:
          type: string
          format: date-time
          nullable: true
          description: When the member was added to the organization.
      example:
        id: Qw8aB2cD3eF4gH5i
        object: team_member
        name: Jordan Rivera
        email: jordan@example.com
        phone: "+15551234567"
        role: manager
        status: active
        emailVerified: true
        createdAt: "2026-06-01T14:32:00.000Z"
    Call:
      type: object
      description: >-
        A call record. Carrier and provider identifiers are deliberately not
        exposed - they are operational internals with no integration use.
      properties:
        id:
          type: string
        object:
          type: string
          example: call
        direction:
          type: string
          nullable: true
          enum:
            - inbound
            - outbound
        from:
          type: string
          description: Originating number in E.164 format.
        to:
          type: string
          description: Destination number in E.164 format.
        customerPhone:
          type: string
          description: The external party's number, whichever leg they were on.
        status:
          type: string
          nullable: true
          description: Final call status reported by the carrier.
        durationSeconds:
          type: integer
          nullable: true
        agentName:
          type: string
          nullable: true
          description: The team member who handled the call, when there was one.
        companyId:
          type: string
          nullable: true
        companyName:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
          nullable: true

    Room:
      type: object
      description: A video room.
      properties:
        id:
          type: string
        object:
          type: string
          example: room
        name:
          type: string
        status:
          type: string
          nullable: true
        participantCount:
          type: integer
          description: >-
            A count rather than the participant list - the underlying array carries
            join metadata that serves no integration purpose.
        activeSessionCount:
          type: integer
        createdAt:
          type: string
          format: date-time
        endedAt:
          type: string
          format: date-time
          nullable: true

    RoomEvent:
      type: object
      description: >-
        A bookable room event type - the kind of meeting an invitee can schedule,
        with its duration and host pool.
      properties:
        id:
          type: string
        object:
          type: string
          example: room_event
        name:
          type: string
        description:
          type: string
        type:
          type: string
          nullable: true
        durationMinutes:
          type: integer
          nullable: true
        hostCount:
          type: integer
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
          nullable: true

    RoomBooking:
      type: object
      description: A scheduled booking against a room event.
      properties:
        id:
          type: string
        object:
          type: string
          example: room_booking
        name:
          type: string
          description: The invitee's name.
        email:
          type: string
          description: The invitee's email address.
        hostName:
          type: string
          nullable: true
        roomId:
          type: string
          nullable: true
        roomType:
          type: string
          nullable: true
        startAt:
          type: string
          nullable: true
          description: When the booking starts.
        durationMinutes:
          type: integer
          nullable: true
        inviteeTimezone:
          type: string
          nullable: true
        notes:
          type: string
        createdAt:
          type: string
          format: date-time
    EngageProfile:
      type: object
      description: The organization's public Engage profile.
      properties:
        id: { type: string }
        object: { type: string, example: engage_profile }
        companyName: { type: string }
        description: { type: string }
        logoUrl: { type: string, nullable: true }
        categories:
          type: array
          items: { type: string }
        services:
          type: array
          items: { type: string }
        reviewCount: { type: integer, nullable: true }
        averageRating: { type: number, nullable: true }
        updatedAt: { type: string, format: date-time }

    EngageReview:
      type: object
      description: >-
        A customer review. The reviewer's email address, IP address and
        geolocation are deliberately never exposed - a review is public, the person
        who left it is not.
      properties:
        id: { type: string }
        object: { type: string, example: engage_review }
        rating: { type: number, nullable: true }
        body: { type: string }
        imageCount: { type: integer }
        authorName: { type: string, description: Display name only, when the reviewer gave one. }
        verified: { type: boolean, description: True when the review is tied to a real job. }
        source: { type: string, nullable: true }
        companyId: { type: string, nullable: true }
        publiclyVisible: { type: boolean }
        reply:
          type: object
          nullable: true
          properties:
            body: { type: string }
            repliedAt: { type: string, format: date-time, nullable: true }
        submittedAt: { type: string, format: date-time }
        createdAt: { type: string, format: date-time }

    EngageForm:
      type: object
      description: A published Engage form.
      properties:
        id: { type: string }
        object: { type: string, example: engage_form }
        name: { type: string }
        description: { type: string }
        leadTableId: { type: string, nullable: true }
        submissionCount: { type: integer, nullable: true }
        lastSubmissionAt: { type: string, format: date-time, nullable: true }
        createdAt: { type: string, format: date-time }

    EngageFormSubmission:
      type: object
      description: >-
        Answers submitted through an Engage form. The visitor's hashed IP is used
        for abuse detection and is not exposed.
      properties:
        id: { type: string }
        object: { type: string, example: engage_form_submission }
        formId: { type: string, nullable: true }
        answers:
          type: object
          additionalProperties: true
          description: The answers as submitted, keyed by field.
        submittedAt: { type: string, format: date-time }

    EngageBooking:
      type: object
      description: A published Engage booking page.
      properties:
        id: { type: string }
        object: { type: string, example: engage_booking }
        name: { type: string }
        description: { type: string }
        companyId: { type: string, nullable: true }
        companyName: { type: string, nullable: true }
        lastSubmissionAt: { type: string, format: date-time, nullable: true }
        createdAt: { type: string, format: date-time }

    EngageBookingSubmission:
      type: object
      description: A booking made through an Engage booking page.
      properties:
        id: { type: string }
        object: { type: string, example: engage_booking_submission }
        bookingId: { type: string, nullable: true }
        appointmentId: { type: string, nullable: true }
        appointmentTypeId: { type: string, nullable: true }
        appointmentTypeName: { type: string, nullable: true }
        name: { type: string }
        email: { type: string }
        phone: { type: string }
        address: { type: string }
        city: { type: string }
        description: { type: string }
        depositAmount: { type: number, nullable: true }
        depositPaid: { type: boolean }
        companyId: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }

    Activity:
      type: object
      description: One entry in a record's change history.
      properties:
        id: { type: string }
        object: { type: string, example: activity }
        entityType: { type: string, nullable: true }
        entityId: { type: string, nullable: true }
        type: { type: string, nullable: true }
        category: { type: string, nullable: true }
        message: { type: string }
        oldValue: { type: string, nullable: true }
        newValue: { type: string, nullable: true }
        source: { type: string, nullable: true }
        createdByName: { type: string, nullable: true, description: Display name of the actor, never their user id. }
        createdAt: { type: string, format: date-time }

    ServiceArea:
      type: object
      description: >-
        A configured service area. Polygon geometry is summarized as point counts
        rather than returned in full - a single metro polygon can be thousands of
        coordinate pairs.
      properties:
        id: { type: string }
        object: { type: string, example: service_area }
        name: { type: string }
        description: { type: string }
        coverageMode: { type: string, nullable: true }
        radiusMiles: { type: number, nullable: true }
        metroZip: { type: string, nullable: true }
        polygonPointCount: { type: integer }
        pinAreaCount: { type: integer }
        color: { type: string, nullable: true }
        status: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time, nullable: true }

    Vendor:
      type: object
      description: A vendor the organization buys from or subcontracts to.
      properties:
        id: { type: string }
        object: { type: string, example: vendor }
        name: { type: string }
        email: { type: string }
        phone: { type: string }
        type: { type: string, nullable: true }
        baseRatePercent: { type: number, nullable: true }
        notes: { type: string }
        logoUrl: { type: string, nullable: true }
        status: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time, nullable: true }

    ItemOrService:
      type: object
      description: >-
        A price-book entry. This is also the stock record: when `inventoryTracked`
        is true, `inventoryQuantities` holds the per-company stock levels.
      properties:
        id: { type: string }
        object: { type: string, example: item_or_service }
        name: { type: string }
        description: { type: string }
        itemType: { type: string, nullable: true }
        sku: { type: string, nullable: true }
        barcode: { type: string, nullable: true }
        manufacturerModel: { type: string, nullable: true }
        unitPrice: { type: number, nullable: true }
        cost: { type: number, nullable: true }
        taxable: { type: boolean }
        categoryId: { type: string, nullable: true }
        category: { type: string, nullable: true }
        inventoryTracked: { type: boolean }
        inventoryQuantities:
          type: object
          additionalProperties: true
          description: Stock on hand, keyed by company.
        continueSellingWhenOutOfStock: { type: boolean }
        status: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time, nullable: true }

    Payment:
      type: object
      description: >-
        A payment against an invoice. Stripe identifiers (customer, account,
        charge, payment intent) are never exposed.
      properties:
        id: { type: string }
        object: { type: string, example: payment }
        invoiceId: { type: string, nullable: true }
        amount: { type: number, nullable: true }
        currency: { type: string }
        method: { type: string, nullable: true }
        provider:
          type: string
          nullable: true
          description: genaya_pay when Genaya processed it, manual when it was recorded from elsewhere.
        status: { type: string, nullable: true }
        fee: { type: number, nullable: true }
        serviceFee: { type: number, nullable: true }
        net: { type: number, nullable: true }
        cardBrand: { type: string, nullable: true }
        last4: { type: string, nullable: true }
        clientName: { type: string, nullable: true }
        clientEmail: { type: string, nullable: true }
        note: { type: string }
        paidOn: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }

    OfflinePaymentCreate:
      type: object
      description: >-
        Parameters for recording a payment taken outside Genaya. Card methods are
        rejected - this endpoint does not move money.
      required:
        - invoiceId
        - amount
        - method
      properties:
        invoiceId:
          type: string
          description: The invoice this payment settles. Must belong to your organization.
        amount:
          type: number
          description: Amount received, in the invoice's currency. Rounded to cents.
        method:
          type: string
          enum:
            - Cash
            - Check
            - Bank Transfer
            - Other
        paidOn:
          type: string
          format: date-time
          description: When the payment was actually received. Defaults to now.
        note:
          type: string
          maxLength: 500

    Commission:
      type: object
      description: A commission earned on an appointment.
      properties:
        id: { type: string }
        object: { type: string, example: commission }
        employeeId: { type: string, nullable: true }
        employeeName: { type: string, nullable: true }
        amount: { type: number, nullable: true }
        currency: { type: string }
        status: { type: string, nullable: true }
        ruleName: { type: string, nullable: true, description: Which rule produced this amount. }
        ruleSource: { type: string, nullable: true }
        appointmentId: { type: string, nullable: true }
        appointmentNumber: { type: integer, nullable: true }
        appointmentTotal: { type: number, nullable: true }
        appointmentScheduledAt: { type: string, format: date-time, nullable: true }
        triggerAt: { type: string, format: date-time, nullable: true }
        eligibleAt: { type: string, format: date-time, nullable: true }
        paidAt: { type: string, format: date-time, nullable: true }
        reversedAt: { type: string, format: date-time, nullable: true }
        reversalReason: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time, nullable: true }

    PayrollRun:
      type: object
      description: >-
        A payroll run and its range totals. Per-employee breakdowns, deductions and
        taxes are not exposed here.
      properties:
        id: { type: string }
        object: { type: string, example: payroll_run }
        status: { type: string, nullable: true }
        frequency: { type: string, nullable: true }
        rangeLabel: { type: string, nullable: true }
        periodStart: { type: string, format: date-time, nullable: true }
        periodEnd: { type: string, format: date-time, nullable: true }
        currency: { type: string }
        employeeCount: { type: integer }
        totals:
          type: object
          nullable: true
          additionalProperties: true
        paymentMethod: { type: string, nullable: true }
        paymentReference: { type: string, nullable: true }
        finalizedAt: { type: string, format: date-time, nullable: true }
        finalizedByName: { type: string, nullable: true }
        paidAt: { type: string, format: date-time, nullable: true }
        paidByName: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time, nullable: true }

    PayPeriod:
      type: object
      description: One employee's pay period.
      properties:
        id: { type: string }
        object: { type: string, example: pay_period }
        employeeId: { type: string, nullable: true }
        employeeName: { type: string, nullable: true }
        label: { type: string, nullable: true }
        status: { type: string, nullable: true }
        frequency: { type: string, nullable: true }
        periodStart: { type: string, format: date-time, nullable: true }
        periodEnd: { type: string, format: date-time, nullable: true }
        weekStart: { type: string, nullable: true }
        runId: { type: string, nullable: true }
        closedAt: { type: string, format: date-time, nullable: true }
        closedByName: { type: string, nullable: true }
        closedReason: { type: string, nullable: true }
        reopenedAt: { type: string, format: date-time, nullable: true }
        reopenReason: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time, nullable: true }
    Message:
      type: object
      description: An SMS or MMS message sent from or received by the organization.
      properties:
        id:
          type: string
          description: Unique identifier of the message.
        object:
          type: string
          example: message
        direction:
          type: string
          description: Whether the message was sent by the organization or received from a contact.
          enum:
            - inbound
            - outbound
        from:
          type: string
          description: The sending phone number in E.164 format.
        to:
          type: string
          description: The receiving phone number in E.164 format.
        body:
          type: string
          description: The message text.
        media:
          type: array
          description: Attached media, when the message is an MMS.
          items:
            type: object
            properties:
              url:
                type: string
              contentType:
                type: string
                nullable: true
        segments:
          type: integer
          nullable: true
          description: >-
            Carrier segments the message was split into. Billing is per segment, so a
            long body costs more than a short one.
        status:
          type: string
          nullable: true
          description: Delivery status reported by the carrier.
        conversationId:
          type: string
          nullable: true
          description: The conversation thread this message belongs to.
        companyId:
          type: string
          nullable: true
          description: The company the sending number belongs to.
        createdAt:
          type: string
          format: date-time

    MessageCreate:
      type: object
      description: Parameters for sending an SMS or MMS.
      required:
        - to
      properties:
        to:
          type: string
          description: Recipient phone number in E.164 format, for example +14155550142.
          example: "+14155550142"
        body:
          type: string
          maxLength: 1600
          description: >-
            The message text. Either body or mediaUrls (or both) must be provided.
            Longer bodies are not truncated - they are billed as more segments.
        mediaUrls:
          type: array
          maxItems: 10
          description: Publicly reachable https:// URLs to attach as MMS media.
          items:
            type: string
            format: uri
        from:
          type: string
          description: >-
            Which of the organization's numbers to send from, in E.164 format. When
            omitted, the company's configured outbound SMS number is used, falling
            back to any SMS-capable number the organization owns.
        companyId:
          type: string
          description: >-
            Send using this company's configured outbound number. Ignored when from
            is supplied explicitly.

    AiAsk:
      type: object
      description: A question for Genaya AI about the organization's own data.
      required:
        - question
      properties:
        question:
          type: string
          maxLength: 2000
          description: A plain-language question about the organization's records.
          example: How many appointments were booked last month?
        tier:
          type: string
          description: >-
            Which model tier to use. Omit to let Genaya route the question
            automatically. A tier outside the plan's allowance is rejected.
          enum:
            - spark
            - brain
            - genius

    AiAnswer:
      type: object
      description: Genaya AI's answer, along with what it consulted to produce it.
      properties:
        object:
          type: string
          example: ai_answer
        question:
          type: string
        answer:
          type: string
          description: The plain-language answer, with any internal formatting markers removed.
        blocks:
          type: array
          description: >-
            Structured output the model chose to emit alongside the prose - stat
            cards, tables or charts. Empty when the answer is plain text.
          items:
            type: object
            additionalProperties: true
        tier:
          type: string
          description: The model tier that answered.
        model:
          type: string
          nullable: true
        collectionsQueried:
          type: array
          description: >-
            Which record types the model actually queried. An empty array means the
            answer was produced without looking anything up.
          items:
            type: string
        toolCallsUsed:
          type: integer
          description: How many data lookups the model performed (capped per question).
    CatalogItem:
      type: object
      description: >-
        A configured option belonging to the organization - an appointment type or
        lead source. Managed in the Genaya app and read-only through the API.
        Tags share this shape and add a colour plus the record types they apply to
        (see Tag).
      properties:
        id:
          type: string
        object:
          type: string
          example: appointment_type
        name:
          type: string
        description:
          type: string
        status:
          type: string
          description: Whether the option is currently active or has been retired.
        displayOrder:
          type: integer
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
          nullable: true

    Tag:
      type: object
      description: >-
        A tag configured by the organization. Same shape as any other configured
        option, plus a colour swatch and the record types it may be applied to.
      allOf:
        - $ref: '#/components/schemas/CatalogItem'
        - type: object
          properties:
            color:
              type: string
              nullable: true
            appliesTo:
              type: array
              description: Which record types this tag can be applied to.
              items:
                type: string

    Company:
      type: object
      description: A company (business unit) inside the organization. Read-only.
      properties:
        id:
          type: string
        object:
          type: string
          example: company
        name:
          type: string
        email:
          type: string
        phone:
          type: string
        timeZone:
          type: string
          nullable: true
        status:
          type: string
        createdAt:
          type: string
          format: date-time

    PhoneNumber:
      type: object
      description: >-
        A phone number the organization owns. Read-only; routing, caller-ID and
        carrier configuration are managed in the app and are not exposed here.
      properties:
        id:
          type: string
        object:
          type: string
          example: phone_number
        phoneNumber:
          type: string
          description: The number in E.164 format.
        name:
          type: string
        companyId:
          type: string
          nullable: true
        status:
          type: string
        capabilities:
          type: array
          description: What the number can do, for example SMS or voice.
          items:
            type: string
        createdAt:
          type: string
          format: date-time

    Account:
      type: object
      description: Identity and capability of the API key making the request.
      properties:
        object:
          type: string
          example: account
        organizationId:
          type: string
        organizationName:
          type: string
          nullable: true
        plan:
          type: string
        apiKey:
          type: object
          properties:
            id:
              type: string
              description: A short, non-reversible identifier for the key. Never the key itself.
            name:
              type: string
              nullable: true
            mode:
              type: string
              enum:
                - live
                - test
            scopes:
              type: array
              description: The scopes this key holds. ["*"] means unrestricted.
              items:
                type: string
            scopesAreUnrestricted:
              type: boolean
            expiresAt:
              type: string
              format: date-time
              nullable: true
            readOnly:
              type: boolean
              description: True for test keys, which may read but never write.

    Usage:
      type: object
      description: >-
        Current consumption against every limit that can reject a request - the two
        rate-limit windows, the organization's daily write quota, and any countable
        plan cap.
      properties:
        object:
          type: string
          example: usage
        organizationId:
          type: string
        plan:
          type: string
        rateLimit:
          type: object
          description: Per API key.
          properties:
            minute:
              $ref: '#/components/schemas/UsageWindow'
            day:
              $ref: '#/components/schemas/UsageWindow'
        writeQuota:
          type: object
          description: Per organization, across every key it holds. Reads never count.
          properties:
            used:
              type: integer
            limit:
              type: integer
            remaining:
              type: integer
            resetsAt:
              type: string
              format: date-time
        planLimits:
          type: object
          description: >-
            Per organization, from the subscription plan. A limit of null with
            unlimited true means the plan does not cap that resource.
          additionalProperties:
            type: object
            properties:
              limit:
                type: integer
                nullable: true
              unlimited:
                type: boolean
              used:
                type: integer
                nullable: true
              remaining:
                type: integer
                nullable: true

    UsageWindow:
      type: object
      properties:
        used:
          type: integer
        limit:
          type: integer
        remaining:
          type: integer
        resetsAt:
          type: string
          format: date-time
