---
openapi: 3.0.1
info:
  title: Decile Hub API v1
  version: v1
  description: The Decile Hub API and Decile Hub Browser Extension use **API keys**,
    which you can view/generate [here](/settings/api) if you are logged into Hub,
    or see above. Once you have an API key, pass it in the `Authorization` header
    of each request. If you have questions, ask in [Decile Base](/base/8) and somebody
    from the dev team will get back to you shortly.
paths:
  "/api/v1/whoami":
    get:
      summary: Identify the authenticated API key
      tags:
      - Authentication
      description: Returns metadata about the authenticated API key, the user it belongs
        to, the account it is scoped to, the key-level permissions, and (for regular
        user keys) the user's account_user role flags and accessible pipeline IDs.
        Useful for agents and integrations to introspect their own capabilities before
        making other calls. Legacy API keys are rejected with a 403.
      security:
      - api_key: []
      responses:
        '200':
          description: API key identified
          content:
            application/json:
              examples:
                user_token:
                  value:
                    token:
                      id: 42
                      name: Default
                      kind: user
                      transient: false
                      expires_at:
                      last_used_at: '2026-04-28T10:00:00Z'
                      permissions: []
                    user:
                      id: 7
                      email: john@example.com
                      first_name: John
                      last_name: Doe
                    account:
                      id: 1
                      name: Example Account
                      subdomain: example
                      currency: USD
                    account_user:
                      is_admin: false
                      roles:
                        admin: false
                        recruiting: false
                        investor: false
                        investment: true
                        connector: false
                        inbox: true
                        data_room: false
                        closing: false
                        clean: false
                        custom: false
                        capital_call: false
                        portfolio: false
                        account_organization_fund: false
                        event_attendance: false
                        directory: true
                        website: false
                      accessible_pipeline_ids:
                      - 42
                      - 99
                admin_token:
                  value:
                    token:
                      id: 99
                      name: Administrate
                      kind: admin
                      transient: false
                      expires_at:
                      last_used_at: '2026-04-28T10:00:00Z'
                      permissions: []
                    user:
                      id: 3
                      email: admin@decile.com
                      first_name: Decile
                      last_name: Admin
                    account:
                      id: customer-account
                      name: Customer Account
                      subdomain: customer-account
                      currency: USD
                    account_user:
        '401':
          description: Unauthorized — API key missing, invalid, or expired
        '403':
          description: Forbidden — legacy API keys are not supported
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                legacy_rejected:
                  value:
                    error:
                      code: forbidden
                      message: This endpoint does not support legacy API keys. Generate
                        a new key at /api/settings and retire the old key.
  "/api/v1/admin/accounts":
    get:
      summary: List accounts the calling admin can access
      tags:
      - Admin
      description: Admin-only. Returns the Decile Hub accounts the calling admin user
        can reach — accounts that have opted in to decile-admin access plus any account
        the admin is directly a member of. Results are sorted by name and capped at
        100 rows; pass `search` to filter by name or subdomain when the admin has
        access to many accounts. Non-admin keys receive 403. Requires `X-Account-Id`
        header set to any account this admin can access; the response then enumerates
        the full set including that seed account.
      security:
      - api_key: []
      parameters:
      - name: search
        in: query
        required: false
        description: Optional case-insensitive substring filter on account name or
          subdomain.
        schema:
          type: string
      responses:
        '200':
          description: accounts returned
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  required:
                  - id
                  - slug
                  - name
                  properties:
                    id:
                      type: integer
                      description: Numeric account id; pass as the `X-Account-Id`
                        header on subsequent admin API calls.
                    slug:
                      type: string
                      description: Account subdomain.
                    name:
                      type: string
                      description: Human-readable account name.
              examples:
                two_accounts:
                  value:
                  - id: 12
                    slug: acme
                    name: Acme Capital
                  - id: 47
                    slug: zenith
                    name: Zenith Ventures
        '401':
          description: Unauthorized — API key missing, invalid, expired, or `X-Account-Id`
            header missing/unauthorized
        '403':
          description: Forbidden — endpoint requires an admin API key
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                non_admin_token:
                  value:
                    error:
                      code: forbidden
                      message: Forbidden
  "/api/v1/accounts":
    get:
      operationId: get_account
      x-agent-tool: true
      summary: Retrieves the current account
      tags:
      - Accounts
      description: Returns the current account's basic information
      security:
      - api_key: []
      responses:
        '200':
          description: account found
        '401':
          description: Unauthorized
  "/api/v1/account_users":
    get:
      operationId: list_account_users
      x-agent-tool: true
      summary: List the AccountUsers (team members) on the current account
      tags:
      - AccountUsers
      description: |
        Returns the AccountUsers belonging to the authenticated API key's account.
        Use to discover the `assigned_id` value for `PATCH /api/v1/pipeline_prospects` and
        `PATCH /api/v1/pipeline_prospects/{id}` (the prospect-owner field).

        Pagination is zero-indexed; page size is 100.
      security:
      - api_key: []
      parameters:
      - name: page
        in: query
        required: false
        description: Zero-indexed page number (default 0)
        schema:
          type: integer
      responses:
        '200':
          description: Account users
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        name:
                          type: string
                        email:
                          type: string
                        title:
                          type: string
                          nullable: true
                        is_admin:
                          type: boolean
                  pagination:
                    type: object
                    properties:
                      total_count:
                        type: integer
                      current_page:
                        type: integer
                      total_pages:
                        type: integer
        '401':
          description: Unauthorized
  "/api/v1/activity_entries":
    get:
      operationId: list_activity_entries
      x-agent-tool: true
      summary: List activity feed entries for the current account
      tags:
      - ActivityEntries
      description: |
        Returns the activity feed for the current account, newest first. Filter by
        subject (entity), event type, actor, or date range.

        Pagination is keyset — pass `pagination.next_page_token` from a response back
        as `page_token=` to fetch the next page.
      security:
      - api_key: []
      parameters:
      - name: subject_type
        in: query
        required: false
        description: Filter to entries whose subject is this top-level entity class.
          Pair with `subject_id`.
        schema:
          type: string
          enum:
          - Person
          - Organization
          - PipelineProspect
          - AccountOrganizationFund
          - AccountOrganizationManagementCompany
          - AccountOrganizationGeneralPartnership
          - FirmAdmin::PortfolioCompany
          - FirmAdmin::CapitalAccount
          - Chat
          - User
      - name: subject_id
        in: query
        required: false
        description: Subject record id; pair with `subject_type`.
        schema:
          type: integer
      - name: include_associated
        in: query
        required: false
        description: When `subject_type=Organization`, also include entries for its
          People. Defaults `false`.
        schema:
          type: boolean
      - name: event
        in: query
        required: false
        description: 'Curated event filter. Mutually exclusive with `entryable_type`
          — sending both returns 400.

          '
        schema:
          type: string
          enum:
          - Item Created
          - Item Edited
          - Prospect Moved
          - All Emails
          - Email Sent
          - Email Received
          - Email Link Clicked
          - PACT Signed
          - Cornerstone Signed
          - Signatory Signed
          - Signing Link Email Sent
          - Onboarding Questionnaire Completed
          - Onboarding Questionnaire Reset
          - Note Added
          - Task Completed
          - File Uploaded
          - AML/KYC
          - Chat Summary
          - Deal Share Copied to Pipeline
      - name: entryable_type
        in: query
        required: false
        description: Lower-level entryable class filter (e.g. `Log::ActivityEntries::Note`).
          Mutually exclusive with `event`.
        schema:
          type: string
      - name: user_id
        in: query
        required: false
        description: Actor user id. Pass the literal `automation` to match entries
          from system/automated events.
        schema:
          type: string
      - name: created_after
        in: query
        required: false
        description: ISO 8601 date or datetime; only entries created on or after this
          point.
        schema:
          type: string
      - name: created_before
        in: query
        required: false
        description: ISO 8601 date or datetime; only entries created on or before
          this point.
        schema:
          type: string
      - name: order_direction
        in: query
        required: false
        description: Sort direction on `(created_at, id)`. Defaults `desc`.
        schema:
          type: string
          enum:
          - asc
          - desc
          default: desc
      - name: page_token
        in: query
        required: false
        description: Opaque keyset cursor returned by a previous call as `pagination.next_page_token`.
        schema:
          type: string
      - name: per_page
        in: query
        required: false
        description: Page size. Capped at 100; defaults to 25.
        schema:
          type: integer
          default: 25
          maximum: 100
      - name: include
        in: query
        required: false
        description: |
          Comma-separated nested embeds. Currently supports `entryable` (returns
          minimal `{type, action}` only).
        schema:
          type: string
      - name: fields
        in: query
        required: false
        description: Comma-separated top-level fields to return. Always returns at
          least `id`. Unknown fields are ignored.
        schema:
          type: string
      responses:
        '200':
          description: paginated list of activity entries
          content:
            application/json:
              examples:
                default:
                  value:
                    data:
                    - id: 12345
                      name: Acme Corp
                      subject_type: Organization
                      subject_id: 678
                      entryable_type: Log::ActivityEntries::Note
                      user_id: 42
                      account_id: 1
                      origin: api
                      created_at: '2026-05-13T18:31:00Z'
                    pagination:
                      next_page_token: eyJjcmVhdGVkX2F0IjoiMjAyNi0wNS0xM1QxODozMTowMFoiLCJpZCI6MTIzNDV9
                      has_more: true
              schema:
                type: object
                required:
                - data
                - pagination
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/ActivityEntry"
                  pagination:
                    type: object
                    properties:
                      next_page_token:
                        type: string
                        nullable: true
                      has_more:
                        type: boolean
        '400':
          description: invalid parameter
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
  "/api/v1/activity_entries/{id}":
    get:
      operationId: get_activity_entry
      x-agent-tool: true
      summary: Get a single activity entry with its full entryable body
      tags:
      - ActivityEntries
      description: |
        Returns one activity entry plus the full per-type `entryable` body —
        audit change diffs, full email bodies and recipients, note bodies,
        AML/KYC status, chat summaries, and so on. The list endpoint omits this detail.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Activity entry id.
        schema:
          type: integer
      responses:
        '200':
          description: the activity entry with its entryable body
          content:
            application/json:
              examples:
                default:
                  value:
                    id: 12345
                    name: Acme Corp
                    subject_type: Organization
                    subject_id: 678
                    entryable_type: Log::ActivityEntries::Note
                    entryable_id: 9999
                    user_id: 42
                    account_id: 1
                    origin: api
                    created_at: '2026-05-13T18:31:00Z'
                    entryable:
                      type: Note
                      body: Followed up via email
              schema:
                "$ref": "#/components/schemas/ActivityEntryDetail"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/networking_fund_events":
    post:
      operationId: create_fund_event
      x-agent-tool: true
      x-requires-approval: true
      summary: Create a networking fund event
      tags:
      - Networking Fund Events
      description: |
        Creates a `Networking::FundEvent` for the authenticated account by
        cloning the account/template default event and applying the given
        fields, mirroring the web event-creation path (including its pipeline +
        questionnaire setup). Requires a caller authorized by
        `Networking::FundEventPolicy`. Only call after the user confirms the
        details via the A2UI form.
      security:
      - api_key: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - landing_page_header_text
              - event_when
              - event_when_timezone
              properties:
                landing_page_header_text:
                  type: string
                  description: Event title.
                landing_page_super_text:
                  type: string
                  description: Short event description or tagline.
                event_when:
                  type: string
                  description: Event date and time as a local datetime string.
                event_when_timezone:
                  type: string
                  description: IANA timezone, e.g. America/New_York.
                event_duration_minutes:
                  type: integer
                  description: Duration in minutes (default 60).
                event_where_name:
                  type: string
                  description: Venue name for an in-person event.
                event_where_address:
                  type: string
                  description: Venue address for an in-person event.
                virtual_link:
                  type: string
                  description: Join link for a virtual event.
                access_type:
                  type: string
                  enum:
                  - public_access
                  - private_access
                  description: public_access or private_access.
                published:
                  type: boolean
                  description: Publish the event (all required fields must be present).
      responses:
        '201':
          description: Created
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (caller not authorized by FundEventPolicy)
        '422':
          description: Validation failure
  "/api/v1/networking_fund_events/{id}":
    patch:
      operationId: update_fund_event
      x-agent-tool: true
      x-requires-approval: true
      summary: Update a networking fund event
      tags:
      - Networking Fund Events
      description: |
        Updates a `Networking::FundEvent` owned by the authenticated account.
        Requires a caller authorized by `Networking::FundEventPolicy`. Only call
        after the user confirms via the A2UI form.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
        description: Id of the fund event to update.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                landing_page_header_text:
                  type: string
                  description: Event title.
                landing_page_super_text:
                  type: string
                  description: Short event description or tagline.
                event_when:
                  type: string
                  description: Event date and time as a local datetime string.
                event_when_timezone:
                  type: string
                  description: IANA timezone, e.g. America/New_York.
                event_duration_minutes:
                  type: integer
                  description: Duration in minutes.
                event_where_name:
                  type: string
                  description: Venue name.
                event_where_address:
                  type: string
                  description: Venue address.
                virtual_link:
                  type: string
                  description: Join link for a virtual event.
                access_type:
                  type: string
                  enum:
                  - public_access
                  - private_access
                  description: public_access or private_access.
                published:
                  type: boolean
                  description: Publish the event.
      responses:
        '200':
          description: Updated
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
        '422':
          description: Validation failure
  "/api/v1/entities":
    get:
      operationId: list_entities
      x-agent-tool: true
      summary: List firm-admin entities
      tags:
      - Entities
      description: |
        Returns a paginated list of the firm-admin entities (funds, SPVs,
        holdings, management companies, general partnerships) under the
        authenticated API key's account. Identity-only payload — fund-specific
        configuration (cutover dates, GP carry %, logo URL) is returned by
        `GET /api/v1/entities/{id}` instead.

        Pagination is 1-indexed (page 1 is the first page). `per_page` is
        capped at 100; values <= 0 fall back to the default of 50.
      security:
      - api_key: []
      parameters:
      - name: type
        in: query
        description: |
          Filter by entity kind. Repeatable — send the param multiple times
          for multiple kinds (`?type=fund&type=spv`). Unknown values return a 400
          `invalid_parameter` error rather than being silently ignored.

          `fund` maps to `AccountOrganizationFund` rows whose
          `fund_details.fund_type` is `fund` or `start_fund`. `spv` maps to
          `fund_details.fund_type=spv`. `holding` maps to
          `fund_details.fund_type=fund_of_funds`.
        required: false
        explode: true
        schema:
          type: array
          items:
            type: string
            enum:
            - fund
            - spv
            - management_company
            - general_partnership
            - holding
      - name: active
        in: query
        description: |
          Active vs hidden entity filter. `true` (default) returns
          active (non-hidden) entities (`hidden_from_left_nav=false`);
          `false` returns only hidden entities; `all` returns both.
        required: false
        schema:
          type: string
          enum:
          - 'true'
          - 'false'
          - all
          default: 'true'
      - name: page
        in: query
        description: 1-indexed page number (default 1).
        required: false
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        description: Page size. Capped at 100; defaults to 50 when omitted or <= 0.
        required: false
        schema:
          type: integer
          default: 50
          maximum: 100
      responses:
        '200':
          description: paginated list of entities
          content:
            application/json:
              schema:
                type: object
                required:
                - entities
                - page
                - per_page
                - total
                properties:
                  entities:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Entity"
                  page:
                    type: integer
                  per_page:
                    type: integer
                  total:
                    type: integer
        '400':
          description: invalid parameter
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
    post:
      operationId: create_entity
      x-agent-tool: true
      summary: Create a firm-admin entity
      tags:
      - Entities
      description: |
        Creates a new `AccountOrganization` plus a fresh `Organization` and (for
        fund kinds) a `FundDetail` under the authenticated account.

        Onboarding side effects are **suppressed** for API-created entities —
        no LP closing pipeline is generated, no `AccountOrganizationUser` is
        attached for the account owner, and no Accelerator client_product
        touch fires. Callers that want the full onboarding workflow should
        continue to use the firm-admin UI.

        POST is not idempotent. To retry safely, GET /api/v1/entities and
        check whether the desired entity already exists before retrying.

        Requires a caller who is a `full_access_account_admin?` of the target account.
      security:
      - api_key: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/EntityCreateRequest"
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/EntityShowResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (caller is not full_access_account_admin)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: Validation failure
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/entities/{id}":
    get:
      operationId: get_entity
      x-agent-tool: true
      summary: Show one firm-admin entity
      tags:
      - Entities
      description: |
        Returns identity + a `details` block for one entity. The `details`
        block always includes currency, currency_symbol, fiscal_year_end,
        entity_name, logo_url, is_active, and kind. When the entity is a fund
        (kind=`fund`, `spv`, or `holding`) the details block additionally
        includes `gp_carry`, `new_management_fee_system_cutover_date`, and
        `new_carry_system_cutover_date`. For non-fund entities those keys are
        omitted entirely.

        Pass `?include=calculations` to embed a `calculations` block of
        fund-level dashboard values. `?window=now` (default) returns
        as-of-today values and includes `capital_activity.invested` plus
        `fees.reserved_for_fees`; `?window=lifetime` returns forecast values
        and omits those two fields entirely. `calculations` is only supported
        on fund-kind entities (kind=`fund`, `spv`, or `holding`); requesting
        it on a non-fund entity returns 404. Numeric values serialize as
        2-decimal-place decimal strings (currency) or 4-decimal-place
        decimal strings (ratios such as `performance.net_irr`); investor
        counts are integers.

        ALWAYS branch on `calculations.available` rather than assuming the
        figure groups are present. It is `false`, with `reason` and `message`
        and no figure groups, for SPV entities whose computed fund-level figures
        do not reconcile with their actual deal economics; those entities return
        200, not 404. No computed fund-level figure is reported for such an
        entity, and none may be estimated or reconstructed from other endpoints
        and presented as one. The `details` block is unaffected by this
        suppression and still carries that entity's own configured values,
        including `target_fund_size`, `gp_carry`, `fund_life_years` and
        `first_close_date` — those are reported inputs, not computed figures, so
        they may be quoted as what they are (a target, a rate, a date) and must
        never be presented as this entity's ACTUAL capital, performance or fee
        figures. Every entity of `kind=spv` is suppressed — both legacy SPVs and
        Start SPVs — so a `kind=spv` entity never carries computed fund-level
        figures. Still branch on the flag rather than on `kind`; the flag is the
        contract.

        Fee semantics, read `fees.note` and `fees.basis` in the response before
        answering any fee question. Nothing in the `fees` group is cash: no
        value there is money disbursed to the GP. `fees.basis` gives each
        field's recognition basis SEPARATELY (it is an object, not one label),
        and ONLY fields marked `accrued_to_date` are expense already incurred;
        `forecast_lifetime`, `next_12_months_fee_reserve` and
        `projected_lifetime_other_fees` are forward-looking projections or
        reserves and must never be described as fees charged, recognized, or
        owed. On a start fund with `window=now`, `management_fees` and
        `admin_fees` are lifetime forecasts while `other_fees` is to date, and
        `fees.basis.total_fees` is then `mixed`. There is no
        accrued-but-unpaid (GP payable) amount and no fees-paid-to-date amount
        anywhere in this payload, so an outstanding or unpaid fee balance
        cannot be derived from it and must never be presented as if it could.
        `fees.reserved_for_fees` is a forward-looking reserve, never an amount
        owed or unpaid, and is excluded from `fees.total_fees`;
        `fees.basis.reserved_for_fees` says what it measures for this fund
        (next 12 months of fees on a standard fund, a projected lifetime
        other-fees figure on a start fund).
        `fees.total_fees` covers management plus admin plus other fees, so a
        management-fee-only figure must be compared against
        `fees.management_fees`, not against `fees.total_fees`.

        Returns 404 (not 403) when the id refers to an entity in a different
        account.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Entity id (plain integer).
        schema:
          type: integer
      - name: include
        in: query
        required: false
        description: |
          Comma-separated list of optional nested resources to embed. Only
          `calculations` is currently supported. Unknown values are
          silently ignored. When omitted, the `calculations` block is not
          present in the response.
        schema:
          type: string
          enum:
          - calculations
      - name: window
        in: query
        required: false
        description: |
          Calculation window for the `calculations` block. Only meaningful
          when `include=calculations` is supplied. `now` (default) returns
          current as-of values; `lifetime` returns forecast values and
          omits `capital_activity.invested` and `fees.reserved_for_fees`.
          Any other value returns 400 `invalid_parameter`.
        schema:
          type: string
          enum:
          - now
          - lifetime
          default: now
      responses:
        '200':
          description: entity found
          content:
            application/json:
              schema:
                allOf:
                - "$ref": "#/components/schemas/Entity"
                - type: object
                  required:
                  - details
                  properties:
                    details:
                      "$ref": "#/components/schemas/EntityDetails"
                    calculations:
                      "$ref": "#/components/schemas/EntityCalculations"
        '400':
          description: Invalid `window` value
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Entity not found (includes `include=calculations` requested
            on a non-fund entity)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
    patch:
      operationId: update_entity
      x-agent-tool: true
      summary: Update a firm-admin entity
      tags:
      - Entities
      description: |
        Updates writable fields on an existing entity within the authenticated
        account. All fields optional — only sent fields are applied.

        `kind` is immutable. Sending a matching `kind` is a no-op; sending a
        differing `kind` returns 422.

        Renaming guard: if the entity's `Organization` is shared with sibling
        AccountOrganization rows (an edge case for entities created before this
        endpoint existed, or via paths that don't use the API's two-step create),
        PATCH refuses with 409 `organization_shared_with_siblings` and includes
        the sibling entity ids in `error.details.sibling_entity_ids` so callers
        can fall back to the firm-admin UI.

        Additional rename guard at the model level:
        `Organization#prevent_name_change_if_accounting_transactions_exist`
        returns 422 if the Organization has associated accounting transactions.

        PATCH `is_active=false` is the v1 stopgap for hiding an entity, since
        DELETE is not yet supported.

        Requires a caller who is a `full_access_account_admin?` of the target account.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: Entity id.
        required: true
        schema:
          type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/EntityUpdateRequest"
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/EntityShowResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Entity not found in the authenticated account
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '409':
          description: Rename would affect sibling entities sharing this Organization
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: Validation failure
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/entities/{entity_id}/capital_summary":
    get:
      operationId: get_capital_summary
      x-agent-tool: true
      summary: Fund-level capital summary (NAV, commitments, period activity)
      tags:
      - Capital Accounts
      description: |
        Fund-level capital figures for one entity, summed across its capital
        accounts. Returns NO per-capital-account rows and NO performance ratios.

        `nav` is the sum of ending balances across the entity's capital
        accounts, i.e. the fund's partners' capital (assets less liabilities).
        `ending_capital_balance` carries the identical figure under the other
        name. Quote either verbatim.

        `partner_type` is REQUIRED and has no default: `limited_partner`,
        `general_partner`, or `all`. An unfiltered blend of GP and LP capital
        overstates the LP figure, so the caller must say which one it wants.
        LP-only and GP-only NAV are both meaningful and both supported.

        `capital_accounts_counted` vs `capital_accounts_in_entity` tells you
        whether these totals span the whole fund. Fund-wide amounts (unrealized
        and realized gain, fees) are allocated to each capital account by its
        commitment share, so a filtered set carries only that set's slice. When
        the two counts differ, say so rather than presenting a subset as the
        fund total.

        IRR, TVPI, DPI, RVPI and MOIC are deliberately absent and are not
        available from any agent-reachable endpoint. Do not compute them from
        these figures.
      security:
      - api_key: []
      parameters:
      - name: entity_id
        in: path
        description: Entity (fund) id.
        required: true
        schema:
          type: integer
      - name: partner_type
        in: query
        required: true
        description: Required. One of limited_partner, general_partner, all.
        schema:
          type: string
          enum:
          - limited_partner
          - general_partner
          - all
      - name: active
        in: query
        required: false
        description: "`true` (default) counts enabled capital accounts only; `false`
          only disabled; `all` both."
        schema:
          type: string
      - name: period_start
        in: query
        required: false
        description: Defaults to fund inception. Must be supplied together with period_end.
        schema:
          type: string
          format: date
      - name: period_end
        in: query
        description: End of the reporting window (ISO 8601 date). Must be supplied
          together with period_start.
        required: false
        schema:
          type: string
          format: date
      responses:
        '200':
          description: fund-level capital summary
          content:
            application/json:
              examples:
                default:
                  value:
                    entity:
                      id: 206166962
                      name: Zebra Fund II
                      kind: fund
                    meta:
                      period_start: '2025-01-01'
                      period_end: '2025-12-31'
                      currency: USD
                    partner_type: limited_partner
                    capital_accounts_counted: 125
                    capital_accounts_in_entity: 128
                    nav:
                      nav: '15601000.0'
                      ending_capital_balance: '15601000.0'
                      as_of: '2025-12-31'
                    commitments:
                      committed_capital: '15601000.0'
                      lp_committed_capital: '15601000.0'
                      gp_committed_capital: '0.0'
                    period_activity:
                      capital_contribution: '0.0'
                      distributions: '0.0'
                      ending_balance: '15601000.0'
        '400':
          description: partner_type missing or invalid, or a malformed period window
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Entity not found
  "/api/v1/entities/{entity_id}/capital_accounts":
    get:
      operationId: list_capital_accounts
      x-agent-tool: true
      summary: List capital accounts under an entity
      tags:
      - Capital Accounts
      description: |
        Identity-plus-commitments list. Each row is the shared identity
        block plus `committed_capital` — the account's commitment tranches
        summed as a decimal string (`"0.0"` when it has none). Other
        per-account calculation values (contributions, balances) are
        intentionally NOT included here — they come from the per-record
        `/calculations` endpoint and the bulk
        `/entities/{entity_id}/capital_accounts/calculations` endpoint.
        The response's top-level `total_committed_capital_all_matches`
        sums commitment tranches across EVERY account matching the
        current filters (not just the page), so commitment totals are
        answerable without paging.

        Optional filters: fuzzy `q` (LP name, fund entity name, or primary
        contact email), `partner_type`, boolean flags
        (`kyc_passed`/`aml_passed`/`transferred`), an `admitted_on` window
        (`admitted_after`/`admitted_before`), and a total-commitment range
        (`min_total_commitment`/`max_total_commitment`). Sort with
        `sort_by`/`sort_order` (default: ascending id).

        Returns 404 when `entity_id` resolves to an entity in a different
        account (cross-account requests are not distinguished from missing).
      security:
      - api_key: []
      parameters:
      - name: entity_id
        in: path
        required: true
        description: Entity id (plain integer).
        schema:
          type: integer
      - name: active
        in: query
        description: |
          `true` (default) returns capital accounts where
          `is_capital_account_enabled=true`. `false` returns disabled
          accounts; `all` returns both.
        required: false
        schema:
          type: string
          enum:
          - 'true'
          - 'false'
          - all
          default: 'true'
      - name: partner_type
        in: query
        description: |
          Optional partner-type filter. Unknown values return a 400
          `invalid_parameter` error.
        required: false
        schema:
          type: string
          enum:
          - limited_partner
          - general_partner
      - name: q
        in: query
        description: |
          Fuzzy match on capital account name, fund entity name, or
          primary contact email (case-insensitive substring).
        required: false
        schema:
          type: string
      - name: kyc_passed
        in: query
        description: Filter by KYC-passed flag (`true`/`false`).
        required: false
        schema:
          type: boolean
      - name: aml_passed
        in: query
        description: Filter by AML-passed flag (`true`/`false`).
        required: false
        schema:
          type: boolean
      - name: transferred
        in: query
        description: Filter by transferred flag (`true`/`false`).
        required: false
        schema:
          type: boolean
      - name: admitted_after
        in: query
        description: ISO date (YYYY-MM-DD) lower bound on `admitted_on`.
        required: false
        schema:
          type: string
          format: date
      - name: admitted_before
        in: query
        description: ISO date (YYYY-MM-DD) upper bound on `admitted_on`.
        required: false
        schema:
          type: string
          format: date
      - name: min_total_commitment
        in: query
        description: |
          Only accounts whose summed commitment tranches exceed this value
          (whole currency units). Accounts with no commitments never match.
        required: false
        schema:
          type: number
      - name: max_total_commitment
        in: query
        description: |
          Only accounts whose summed commitment tranches are below this
          value (whole currency units).
        required: false
        schema:
          type: number
      - name: sort_by
        in: query
        description: |
          Sort field. Omitted = original ascending-id order. Unknown
          values return a 400 `invalid_parameter` error.
          `total_commitment` orders by each account's summed kept
          commitment tranches; accounts with no commitments sort last.
        required: false
        schema:
          type: string
          enum:
          - name
          - created_at
          - admitted_on
          - total_commitment
      - name: sort_order
        in: query
        description: Sort direction when `sort_by` is supplied. Default asc.
        required: false
        schema:
          type: string
          enum:
          - asc
          - desc
      - name: page
        in: query
        description: 1-indexed page number (default 1).
        required: false
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        description: Page size. Capped at 100; defaults to 50.
        required: false
        schema:
          type: integer
          default: 50
          maximum: 100
      responses:
        '200':
          description: paginated list of capital accounts
          content:
            application/json:
              schema:
                type: object
                required:
                - capital_accounts
                - page
                - per_page
                - total
                properties:
                  capital_accounts:
                    type: array
                    items:
                      "$ref": "#/components/schemas/CapitalAccountListRow"
                  page:
                    type: integer
                  per_page:
                    type: integer
                  total:
                    type: integer
                  total_committed_capital_all_matches:
                    type: string
                    description: |
                      Decimal string. Commitment tranches summed across every
                      capital account matching the current filters — not just
                      this page.
        '400':
          description: invalid parameter
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Entity not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/entities/{entity_id}/bank_transactions":
    get:
      operationId: list_bank_transactions
      x-agent-tool: true
      summary: List imported bank transactions under an entity
      tags:
      - Bank Transactions
      description: |
        Read-only discovery of imported bank transactions. The privacy-safe
        response omits sensitive provider metadata, account and routing data,
        external identifiers, GL preassignments, counterparties, and
        supersession identifiers. Discarded records are excluded. This endpoint
        cannot create, import, sync, update, reconcile, discard, or delete data.
        Unknown and cross-account entity ids both return 404.
      security:
      - api_key: []
      parameters:
      - name: entity_id
        in: path
        description: Entity (fund) id.
        required: true
        schema:
          type: integer
      - name: provider
        in: query
        description: Filter by the banking data provider the transaction came from.
        schema:
          type: string
          enum:
          - plaid
          - mercury
          - grasshopper_treasury_prime
          - citizens_fdx
          - svb
      - name: status
        in: query
        description: Filter by transaction status.
        schema:
          type: string
          enum:
          - pending
          - posted
          - void
          - superseded
      - name: reconciliation_status
        in: query
        description: Reconciled journal entry, unreconciled journal entry, or no journal
          entry.
        schema:
          type: string
          enum:
          - reconciled
          - unreconciled
          - unavailable
      - name: transaction_at_start
        in: query
        description: Only transactions on or after this date (ISO 8601).
        schema:
          type: string
          format: date
      - name: transaction_at_end
        in: query
        description: Only transactions on or before this date (ISO 8601).
        schema:
          type: string
          format: date
      - name: min_amount
        in: query
        description: Signed transaction amount lower bound.
        schema:
          type: number
      - name: max_amount
        in: query
        description: Signed transaction amount upper bound.
        schema:
          type: number
      - name: bank_account_id
        in: query
        description: Filter to a single bank account by id.
        schema:
          type: integer
      - name: page
        in: query
        description: 1-indexed page number (default 1).
        schema:
          type: integer
          default: 1
          minimum: 1
      - name: per_page
        in: query
        description: Page size. Capped at 100; defaults to 50.
        schema:
          type: integer
          default: 50
          maximum: 100
      responses:
        '200':
          description: Paginated bank transactions
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/BankTransactionListResponse"
        '400':
          description: Invalid filter
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Entity not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/bank_transactions/{id}":
    get:
      operationId: get_bank_transaction
      x-agent-tool: true
      summary: Get one imported bank transaction
      tags:
      - Bank Transactions
      description: |
        Read-only lookup using the same privacy-safe allowlist as the list
        endpoint. Sensitive provider metadata, bank identifiers, and internal
        matching fields are omitted. This endpoint performs no writes, imports,
        synchronization, reconciliation, updates, discards, or deletion.
        Unknown, discarded, and cross-account ids all return 404.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: Bank transaction id.
        required: true
        schema:
          type: integer
      responses:
        '200':
          description: Bank transaction found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/BankTransactionResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Bank transaction not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/entities/{entity_id}/journal_entries":
    get:
      operationId: list_journal_entries
      x-agent-tool: true
      summary: List journal entries (GL transactions) under an entity
      tags:
      - Journal Entries
      description: |
        Returns the journal entries (general-ledger / accounting transactions —
        double-entry GL postings) recorded under the given entity. Each row
        carries the transaction date/amount, reconciled/pending flags, comment,
        the debit and credit ledger accounts (code + name), and the polymorphic
        counterparty (type, id, name; null when absent). Discarded entries are
        excluded.

        Filters (all optional): a `transaction_at` window via
        `transaction_at_start`/`transaction_at_end`; `debit_code`/`credit_code`
        (GL account codes — an unknown code returns no rows); `reconciled` and
        `pending` (true/false); `min_amount`/`max_amount`; and
        `counterparty_type` (+ optional `counterparty_id`).

        Returns 404 when `entity_id` resolves to an entity in a different
        account (cross-account requests are not distinguished from missing).
      security:
      - api_key: []
      parameters:
      - name: entity_id
        in: path
        required: true
        description: Entity id (plain integer).
        schema:
          type: integer
      - name: transaction_at_start
        in: query
        required: false
        description: ISO date (YYYY-MM-DD) lower bound on `transaction_at`.
        schema:
          type: string
          format: date
      - name: transaction_at_end
        in: query
        required: false
        description: ISO date (YYYY-MM-DD) upper bound on `transaction_at`.
        schema:
          type: string
          format: date
      - name: debit_code
        in: query
        required: false
        description: GL account code on the debit side (e.g. "1100"). Unknown codes
          return no rows.
        schema:
          type: string
      - name: credit_code
        in: query
        required: false
        description: GL account code on the credit side (e.g. "6000"). Unknown codes
          return no rows.
        schema:
          type: string
      - name: reconciled
        in: query
        required: false
        description: Filter by reconciled flag. Unknown values return a 400 `invalid_parameter`
          error.
        schema:
          type: string
          enum:
          - 'true'
          - 'false'
      - name: pending
        in: query
        required: false
        description: Filter by pending flag. Unknown values return a 400 `invalid_parameter`
          error.
        schema:
          type: string
          enum:
          - 'true'
          - 'false'
      - name: min_amount
        in: query
        required: false
        description: Lower bound on `transaction_amount`.
        schema:
          type: number
      - name: max_amount
        in: query
        required: false
        description: Upper bound on `transaction_amount`.
        schema:
          type: number
      - name: counterparty_type
        in: query
        required: false
        description: Polymorphic counterparty type (e.g. FirmAdmin::CapitalAccount,
          FirmAdmin::PortfolioCompany, Organization).
        schema:
          type: string
      - name: counterparty_id
        in: query
        required: false
        description: Counterparty id; only meaningful alongside counterparty_type.
        schema:
          type: integer
      - name: page
        in: query
        required: false
        description: 1-indexed page number (default 1).
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        required: false
        description: Page size. Capped at 100; defaults to 50.
        schema:
          type: integer
          default: 50
          maximum: 100
      responses:
        '200':
          description: paginated list of journal entries
          content:
            application/json:
              schema:
                type: object
                required:
                - journal_entries
                - page
                - per_page
                - total
                properties:
                  journal_entries:
                    type: array
                    items:
                      "$ref": "#/components/schemas/JournalEntry"
                  page:
                    type: integer
                  per_page:
                    type: integer
                  total:
                    type: integer
        '400':
          description: invalid parameter
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Entity not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
    post:
      operationId: create_journal_entry
      x-agent-tool: true
      summary: Create one journal entry (GL transaction) under an entity
      tags:
      - Journal Entries
      description: |
        Creates a single journal entry (general-ledger / accounting transaction —
        a double-entry GL posting) under the given entity. Posts one debit/credit
        pair for the supplied amount and date. The entry is created unreconciled
        (`reconciled` false, `pending` false). `debit_code` and `credit_code` are
        GL account codes (see the chart-of-accounts endpoint) — they must differ
        and both must resolve to a known accounting account. `amount` must be
        numeric and greater than 0. `transaction_at` is an ISO date (YYYY-MM-DD).
        An optional counterparty can be attached via `counterparty_type` +
        `counterparty_id`; the counterparty must belong to the same account.

        Returns 422 on invalid input (unknown code, equal debit/credit codes,
        non-positive or non-numeric amount, malformed date, or a cross-account
        counterparty), 403 for a read-only key, and 404 when `entity_id`
        resolves to an entity in a different account.
      security:
      - api_key: []
      parameters:
      - name: entity_id
        in: path
        required: true
        description: Entity id (plain integer).
        schema:
          type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - journal_entry
              properties:
                journal_entry:
                  "$ref": "#/components/schemas/JournalEntryCreateRequest"
      responses:
        '201':
          description: the created journal entry
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/JournalEntry"
        '400':
          description: invalid parameter
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (read-only key or insufficient permission)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Entity not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/entities/{entity_id}/journal_entries/bulk":
    post:
      operationId: bulk_create_journal_entries
      x-agent-tool: true
      summary: Create many journal entries (GL transactions) under an entity (atomic)
      tags:
      - Journal Entries
      description: |
        Creates many journal entries (general-ledger / accounting transactions —
        double-entry GL postings) under the given entity in a single atomic,
        all-or-nothing batch. Each row in `journal_entries` is a full independent
        entry validated by the same rules as the single-create endpoint:
        `debit_code` and `credit_code` are GL account codes (see the
        chart-of-accounts endpoint) that must differ and both resolve to a known
        account, `amount` is numeric and greater than 0, `transaction_at` is an
        ISO date (YYYY-MM-DD), and an optional counterparty may be attached via
        `counterparty_type` + `counterparty_id` (same account).

        Provide between 1 and 100 rows. EVERY row is validated first; if ANY row
        is invalid the entire batch is rejected with 422 and a per-row `errors`
        array (`{index, field, message}`) and NOTHING is created. When all rows
        are valid they are created together inside one database transaction; any
        unexpected failure rolls the whole batch back (never a partial commit).
        All entries are created unreconciled (`reconciled` false, `pending`
        false). Idempotency is the caller's responsibility — re-sending a batch
        posts it again.

        Returns 422 on validation failure or an out-of-range batch (empty or more
        than 100 rows), 403 for a read-only key, and 404 when `entity_id`
        resolves to an entity in a different account.
      security:
      - api_key: []
      parameters:
      - name: entity_id
        in: path
        required: true
        description: Entity id (plain integer).
        schema:
          type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/JournalEntryBulkCreateRequest"
      responses:
        '201':
          description: the created journal entries
          content:
            application/json:
              schema:
                type: object
                required:
                - journal_entries
                - created_count
                properties:
                  journal_entries:
                    type: array
                    items:
                      "$ref": "#/components/schemas/JournalEntry"
                  created_count:
                    type: integer
                    description: Number of journal entries created in the batch.
        '401':
          description: Unauthorized
        '403':
          description: Forbidden (read-only key or insufficient permission)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Entity not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: validation failed (per-row errors, or an out-of-range batch)
          content:
            application/json:
              schema:
                oneOf:
                - "$ref": "#/components/schemas/JournalEntryBulkErrors"
                - "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/journal_entries/{id}":
    get:
      operationId: get_journal_entry
      x-agent-tool: true
      summary: Show one journal entry (GL transaction)
      tags:
      - Journal Entries
      description: |
        Returns a single journal entry (general-ledger / accounting transaction —
        a double-entry GL posting): transaction date/amount, reconciled/pending
        flags, comment, the debit and credit ledger accounts (code + name), and
        the polymorphic counterparty (type, id, name; null when absent). Returns
        404 when the entry is unknown, discarded, or belongs to a different
        account (cross-account requests are not distinguished from missing).
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Journal entry id (plain integer).
        schema:
          type: integer
      responses:
        '200':
          description: journal entry found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/JournalEntry"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Journal entry not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/capital_accounts/{id}":
    get:
      operationId: get_capital_account
      x-agent-tool: true
      summary: Show one capital account
      tags:
      - Capital Accounts
      description: |
        Identity block + commitments + primary_contact + contacts + entity
        ref. `contacts` is the full list of contacts on the capital account
        (joint signatories, spouses, trustees, entity owners/signatories,
        trust beneficiaries); each entry carries role flags plus
        `contact_status_type` (`none`/`primary`/`additional`). Returns 404
        when the capital account belongs to a different account. Use
        `?include=transfers` to embed an ordered list of capital-account
        transfers oriented at the requested CA (`kind=outbound` when the
        requested CA is the `from_capital_account`, `inbound` otherwise);
        `effective_at` is aliased from the underlying `transferred_at`
        column.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Capital account id (plain integer).
        schema:
          type: integer
      - name: include
        in: query
        description: |
          Comma-separated list of nested resources to embed. Only
          `transfers` is currently supported. Unknown values are silently
          ignored. When omitted, the `transfers` array is not present in
          the response.
        required: false
        schema:
          type: string
      responses:
        '200':
          description: capital account found
          content:
            application/json:
              schema:
                allOf:
                - "$ref": "#/components/schemas/CapitalAccountIdentity"
                - type: object
                  required:
                  - commitments
                  - primary_contact
                  - contacts
                  - entity
                  properties:
                    commitments:
                      type: array
                      items:
                        "$ref": "#/components/schemas/Commitment"
                    primary_contact:
                      "$ref": "#/components/schemas/PrimaryContact"
                    contacts:
                      type: array
                      description: |
                        Full list of contacts on the capital account, ordered by
                        created_at. Includes the primary contact plus joint
                        signatories, spouses, trustees, entity owners/signatories,
                        and trust beneficiaries.
                      items:
                        "$ref": "#/components/schemas/CapitalAccountContact"
                    entity:
                      "$ref": "#/components/schemas/EntityRef"
                    transfers:
                      type: array
                      description: Present only when `?include=transfers` is supplied.
                      items:
                        "$ref": "#/components/schemas/CapitalAccountTransfer"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Capital account not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/capital_accounts/{id}/calculations":
    get:
      operationId: get_capital_account_calculations
      x-agent-tool: true
      summary: Single-capital-account period calculations
      tags:
      - Capital Accounts
      description: |
        Computes a single capital account's period figures. Response groups:

        - `commitments` — 12 commitment + paid-in fields.
        - `period_activity` — 15 ledger movements within the window.
        - `performance` — `dpi`, `rvpi`, `tvpi`.
        - `meta` — `period_start`, `period_end`, `currency`,
          `management_fee_system`, `carry_system`, `cutover_dates`. The
          `*_system` keys are `legacy` (window ends before the cutover),
          `new` (window starts on/after the cutover), or `hybrid` (window
          straddles the cutover).

        All numeric values are returned as decimal strings (currency 2
        decimal places, ratios 4 decimal places). Nil values are returned
        as `null`. Returning decimal strings is intentional so callers
        don't lose precision via JS number parsing.

        Period semantics: pass both `period_start` and `period_end` (ISO
        `YYYY-MM-DD`). Both are required together — supplying one without
        the other returns a 400. `period_end` must be on or after
        `period_start`. When neither is supplied, the window defaults to
        the current fiscal-year-to-date for the parent entity.

        In a hybrid window the API uses the new-system carry calculation
        directly even for GP-carry capital accounts, which can differ from
        calculating each side of the cutover separately. Request explicit
        pre/post cutover windows if you need each side calculated on its own.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Capital account id (plain integer).
        schema:
          type: integer
      - name: period_start
        in: query
        description: |
          ISO 8601 date (YYYY-MM-DD). Required when `period_end` is
          supplied. Omitting both defaults to the fund's current
          fiscal-year-to-date.
        required: false
        schema:
          type: string
          format: date
      - name: period_end
        in: query
        description: |
          ISO 8601 date (YYYY-MM-DD). Required when `period_start` is
          supplied. Must be on or after `period_start`.
        required: false
        schema:
          type: string
          format: date
      responses:
        '200':
          description: calculations computed
          content:
            application/json:
              schema:
                type: object
                required:
                - commitments
                - period_activity
                - performance
                - meta
                properties:
                  commitments:
                    "$ref": "#/components/schemas/PeriodCommitments"
                  period_activity:
                    "$ref": "#/components/schemas/PeriodActivity"
                  performance:
                    "$ref": "#/components/schemas/PeriodPerformance"
                  meta:
                    "$ref": "#/components/schemas/PeriodMeta"
        '400':
          description: invalid parameter (e.g. malformed date, end before start)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Capital account not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/entities/{entity_id}/capital_accounts/calculations":
    get:
      operationId: bulk_capital_account_calculations
      x-agent-tool: true
      summary: Bulk capital-account period calculations + totals
      tags:
      - Capital Accounts
      description: |
        Paginated bulk variant of the single-CA calculations endpoint.
        Each row in `capital_accounts` is the same shape as the single-CA
        response (commitments/period_activity/performance/meta groups) but
        with id+name pulled out of identity for easier table rendering.
        Adds a fund-level `totals` block aggregated across the filtered
        scope (NOT only the current page).

        Totals semantics:
        - Currency fields are summed across the filtered scope.
        - Per-CA ratio fields (`percentage_of_total_commitments`,
          `percentage_of_lp_commitments`, `percentage_of_funds_called`,
          `amount_paid_in_for_capital_accounts_percentage`) are `null` in
          totals — per-account ratios do not sum into a fund-level figure.
        - Performance ratios (`dpi`, `rvpi`, `tvpi`) are `null` in totals
          for the same reason.
        - `total_committed_capital`, `lp_committed_capital`,
          `gp_committed_capital` are fund-wide regardless of filters — the
          per-row `total_committed_capital` denominator is a fund-level
          value, not "total of the filtered list." These fund-wide fields
          count commitment tranches whose capital account had a signed LPA
          (timestamped by `period_end`, or carrying the Cornerstone signed flag
          when no timestamp exists) or was enabled and admitted by `period_end`;
          PACT-only and unconfirmed prospects remain available as separate rows
          with `active=all`.

        Keep fundraising stages separate. From `/pipelines?kind=investor`, collect
        every row whose `entity_id` exactly matches the requested entity, then
        pass each matching row's `id` as `/pacts` `pipeline_id` with
        `signed_before=period_end`; an entity can have multiple investor
        pipelines. Each `total_pledged_amount_reportable` is the gross reportable soft-
        commitment total for one pipeline and must not be added to committed
        capital. Reporting status alone does not remove PACT signers who later
        signed an LPA or were admitted, so an outstanding non-binding PACT total
        requires fetching every PACT page for every matching pipeline, retaining
        only `ignore_for_reporting=false` rows, combining them, and calling the capital-account show
        endpoint, and reconciling PACT `person_id` values against contact
        `person_id` values before excluding overlaps. A null PACT `person_id`
        cannot be reconciled deterministically from these APIs; resolve it
        manually or report that an exact outstanding total is unavailable.
        To split binding capital into signed-not-closed and closed/admitted,
        fetch every page of `/entities/{entity_id}/capital_accounts?active=all`
        for identity and every page of this calculation endpoint with
        `active=all` for period-bounded amounts, then join by capital-account
        `id`. Do not use the identity-list row's all-time `committed_capital`
        for a historical window. Define `signed_by_period` as
        `lp_signature_at <= period_end`, plus undated rows where `lpa_signed`
        is true; define `admitted_by_period` as `admitted_on <= period_end`.
        Binding rows satisfy `signed_by_period OR (enabled AND admitted_by_period)`.
        Split those rows into closed/admitted when `admitted_by_period` is true
        and signed-not-closed otherwise; a currently disabled but signed/admitted
        account remains in the closed bucket. Sum the joined calculation row's `committed_capital` for
        each category, and use the complete identity set when reconciling PACT
        overlaps. Capital currently
        available to deploy comes from `/entities/{entity_id}?include=calculations&window=now`
        as `calculations.investable_capital`; do not derive it by applying a
        fixed percentage to committed capital. Branch on `calculations.available`
        there: on an SPV it is `false` and `investable_capital` is absent
        entirely, so no SPV has a computed fund-level investable-capital figure —
        say so rather than falling back to a percentage of committed capital or
        any other derived estimate. This holds for every `kind=spv` entity, both
        legacy SPVs and Start SPVs; still read the flag rather than inferring the
        outcome from the entity's kind.

        Period and decimal-string semantics are identical to the single-CA
        endpoint.

        `fields=` projection: comma-separated whitelist of calculation
        field names. When supplied, rows AND totals are narrowed to just
        the requested fields. If zero requested fields fall within a
        group, that entire group key is dropped from both rows and totals.
        `id` and `name` are always present on each row. Unknown field
        names are silently dropped.
      security:
      - api_key: []
      parameters:
      - name: entity_id
        in: path
        required: true
        description: Entity id (plain integer).
        schema:
          type: integer
      - name: period_start
        in: query
        description: ISO 8601 date. Required when `period_end` is supplied. Defaults
          to fiscal-year-to-date.
        required: false
        schema:
          type: string
          format: date
      - name: period_end
        in: query
        description: ISO 8601 date. Required when `period_start` is supplied. Must
          be on or after `period_start`.
        required: false
        schema:
          type: string
          format: date
      - name: active
        in: query
        description: |
          `true` (default) restricts to enabled capital accounts; `false`
          for disabled-only; `all` for both. The filter narrows the rows
          returned AND the totals aggregation, EXCEPT for the fund-wide
          commitment denominators noted above.
        required: false
        schema:
          type: string
          enum:
          - 'true'
          - 'false'
          - all
          default: 'true'
      - name: partner_type
        in: query
        description: Filter by partner type.
        required: false
        schema:
          type: string
          enum:
          - limited_partner
          - general_partner
      - name: fields
        in: query
        description: |
          Comma-separated whitelist of calculation fields to return.
          When supplied, rows and totals are narrowed to just these
          fields; groups with zero matching fields are dropped from both
          rows and totals. `id` and `name` are always returned on each
          row. Allowed fields (group in parentheses):

          commitments — `committed_capital`, `total_committed_capital`,
          `lp_committed_capital`, `gp_committed_capital`,
          `percentage_of_total_commitments`,
          `percentage_of_lp_commitments`,
          `percentage_of_funds_called`,
          `amount_paid_in_for_capital_accounts`,
          `amount_paid_in_for_capital_accounts_percentage`,
          `outstanding_capital_contribution`,
          `capital_call_receivable`, `capital_call_prepaid`.

          period_activity — `beginning_balance`, `capital_contribution`,
          `syndication_cost`, `management_fees`, `incentive_fees`,
          `net_operating_gain`, `late_interest`, `unrealized_gain`,
          `carried_interest_accrued`, `net_realized_gain`,
          `deemed_gain`, `distributions`, `in_kind_distributions`,
          `transfers`, `ending_balance`.

          performance — `dpi`, `rvpi`, `tvpi`.

          meta — `period_start`, `period_end`, `currency`,
          `management_fee_system`, `carry_system`, `cutover_dates`.
        required: false
        schema:
          type: string
      - name: page
        in: query
        description: 1-indexed page number (default 1).
        required: false
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        description: Page size. Capped at 100; defaults to 50.
        required: false
        schema:
          type: integer
          default: 50
          maximum: 100
      responses:
        '200':
          description: bulk calculations + totals
          content:
            application/json:
              schema:
                type: object
                required:
                - capital_accounts
                - totals
                - page
                - per_page
                - total
                properties:
                  capital_accounts:
                    type: array
                    items:
                      "$ref": "#/components/schemas/BulkCalculationsRow"
                  totals:
                    "$ref": "#/components/schemas/BulkCalculationsTotals"
                  page:
                    type: integer
                  per_page:
                    type: integer
                  total:
                    type: integer
        '400':
          description: invalid parameter
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Entity not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/accounting_accounts":
    get:
      operationId: list_accounting_accounts
      x-agent-tool: true
      summary: List the chart of accounts / GL account codes
      tags:
      - Accounting Accounts
      description: |
        Returns the chart of accounts (GL account codes) shared across the
        account. Use this to discover valid account codes before posting
        journal entries. Codes follow standard GL ranges: 1xxx=assets,
        2xxx=liabilities, 3xxx=equity, 4xxx=revenue, 6xxx/7xxx=expenses. All
        filters are optional and combine with AND. Results are paginated and
        ordered by code.
      security:
      - api_key: []
      parameters:
      - name: code
        in: query
        required: false
        description: Exact account code to match, e.g. "1000".
        schema:
          type: string
      - name: code_prefix
        in: query
        required: false
        description: Match account codes beginning with these characters, e.g. "1"
          for the asset range.
        schema:
          type: string
      - name: is_1099_eligible
        in: query
        required: false
        description: Filter to accounts that are (true) or are not (false) 1099-eligible.
        schema:
          type: boolean
      - name: search
        in: query
        required: false
        description: Case-insensitive substring match against account name or description.
        schema:
          type: string
      - name: page
        in: query
        description: 1-indexed page number (default 1).
        required: false
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        description: Page size. Capped at 100; defaults to 50.
        required: false
        schema:
          type: integer
          default: 50
          maximum: 100
      responses:
        '200':
          description: paginated list of accounting accounts
          content:
            application/json:
              schema:
                type: object
                required:
                - accounting_accounts
                - page
                - per_page
                - total
                properties:
                  accounting_accounts:
                    type: array
                    items:
                      "$ref": "#/components/schemas/AccountingAccount"
                  page:
                    type: integer
                  per_page:
                    type: integer
                  total:
                    type: integer
        '400':
          description: Invalid query parameter
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
  "/api/v1/accounting_accounts/{id}":
    get:
      operationId: get_accounting_account
      x-agent-tool: true
      summary: Show one chart-of-accounts / GL account code
      tags:
      - Accounting Accounts
      description: |
        Returns a single chart-of-accounts entry (GL account code) by its
        integer id. Use after list_accounting_accounts to confirm a code
        before posting a journal entry.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Accounting account id (plain integer).
        schema:
          type: integer
      responses:
        '200':
          description: accounting account found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/AccountingAccount"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Accounting account not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/capital_calls":
    get:
      operationId: list_capital_calls
      x-agent-tool: true
      summary: List capital calls under an entity
      tags:
      - Capital Calls
      description: |
        Returns the capital calls (draft/open/closed) issued from the given
        entity's fund. Each row carries header fields: id, name, status,
        percentage_to_call, notes, amount_being_called (sum of per-LP details),
        created_at, closed_at, due_date, and the parent entity ref.

        Filter by `status` and a date window via `period_start` /
        `period_end` (both ISO dates; supply together). The window bounds
        `created_at` by default; pass `date_field=closed_at` to bound close
        dates instead (draft/open calls have no closed_at and drop out).

        Per-LP wire line items: pass `lp_query` (partial LP name) to keep
        ONLY calls containing a matching LP — the filter applies before
        pagination, so an older matching call is never crowded out by newer
        non-matching calls — and each returned row embeds `lp_details` for
        the matching LPs (amount called, wired amount, wire date, variance,
        payment_status). `include_lp_details=true` embeds `lp_details` for
        every returned call without filtering. Embedded line items cap at 25
        per call; use `/api/v1/capital_calls/{id}/details` for full per-LP
        paging.

        Ordering is deterministic: calls come back newest first
        (`created_at DESC, id DESC`) and `lp_details` rows by `id` — both
        total orders, so identical arguments always return identical rows in
        an identical sequence.

        The header money figures (`amount_being_called`, `total_wired`,
        `total_outstanding`, `total_variance`, `percentage_funded`) ALWAYS
        cover every LP on the call. They are NOT narrowed by `lp_query` and
        NOT shrunk by the 25-row cap, so summing `lp_details` will not
        reproduce them whenever the roster is filtered or truncated. Each row
        carrying `lp_details` also carries `lp_details_coverage`
        (`returned`, `matched`, `call_total`, `truncated`,
        `filtered_by_lp_query`, `totals_basis`) which states exactly that
        relationship — read it before describing the roster.

        Returns 404 when `entity_id` belongs to a different account.
      security:
      - api_key: []
      parameters:
      - name: entity_id
        in: query
        required: true
        description: Entity id (plain integer).
        schema:
          type: integer
      - name: status
        in: query
        required: false
        description: Filter by lifecycle status.
        schema:
          type: string
          enum:
          - draft
          - open
          - closed
      - name: period_start
        in: query
        required: false
        description: ISO date (YYYY-MM-DD) lower bound on `date_field` (default created_at).
          Required together with period_end.
        schema:
          type: string
          format: date
      - name: period_end
        in: query
        required: false
        description: ISO date (YYYY-MM-DD) upper bound on `date_field` (default created_at).
          Required together with period_start.
        schema:
          type: string
          format: date
      - name: date_field
        in: query
        required: false
        description: |
          Which date column the period window bounds. Default created_at.
          `closed_at` targets close dates — draft/open calls (no closed_at)
          drop out of such windows.
        schema:
          type: string
          enum:
          - created_at
          - closed_at
          default: created_at
      - name: lp_query
        in: query
        required: false
        description: |
          Partial LP (capital account) name. Keeps only calls containing a
          matching LP — applied BEFORE pagination — and embeds `lp_details`
          for the matching LPs on each returned row.
        schema:
          type: string
      - name: include_lp_details
        in: query
        required: false
        description: |
          `true` embeds `lp_details` (all LPs, capped at 25 per call) on
          every returned row.
        schema:
          type: boolean
      - name: page
        in: query
        description: 1-indexed page number (default 1).
        required: false
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        description: Page size. Capped at 100; defaults to 50.
        required: false
        schema:
          type: integer
          default: 50
          maximum: 100
      responses:
        '200':
          description: paginated list of capital calls
          content:
            application/json:
              schema:
                type: object
                required:
                - capital_calls
                - page
                - per_page
                - total
                properties:
                  capital_calls:
                    type: array
                    items:
                      allOf:
                      - "$ref": "#/components/schemas/CapitalCallHeader"
                      - type: object
                        properties:
                          lp_details:
                            type: array
                            description: |
                              Present only when `lp_query` or
                              `include_lp_details` is supplied. Per-LP line
                              items ordered by `id` (a total order) and
                              capped at 25 per call.
                            items:
                              type: object
                              properties:
                                capital_account:
                                  type: object
                                  nullable: true
                                  properties:
                                    id:
                                      type: integer
                                    name:
                                      type: string
                                      nullable: true
                                amount_being_called:
                                  type: string
                                called_at:
                                  type: string
                                  format: date-time
                                  description: |
                                    When THIS LP was added to the call. The
                                    only LP-level call timestamp — the
                                    header `created_at` is when the CALL was
                                    created and, on a `standing_call` fund,
                                    precedes every LP's signature.
                                wired_amount:
                                  type: string
                                wire_date:
                                  type: string
                                  nullable: true
                                variance:
                                  type: string
                                payment_status:
                                  type: string
                          lp_details_coverage:
                            type: object
                            description: |
                              Present whenever `lp_details` is. States how
                              the embedded roster relates to the population
                              the header money figures cover, so a partial
                              roster is never mistaken for the full one.
                            required:
                            - returned
                            - matched
                            - call_total
                            - truncated
                            - filtered_by_lp_query
                            - totals_basis
                            properties:
                              returned:
                                type: integer
                                description: Number of rows in `lp_details`.
                              matched:
                                type: integer
                                description: LP rows matching the current `lp_query`
                                  (equals `call_total` when no `lp_query`).
                              call_total:
                                type: integer
                                description: Every LP row on the call — the population
                                  the header money figures are computed over.
                              truncated:
                                type: boolean
                                description: "`matched > returned`: the 25-row cap
                                  dropped matching LPs."
                              filtered_by_lp_query:
                                type: boolean
                                description: An `lp_query` narrowed the roster, so
                                  it is a subset of the call.
                              totals_basis:
                                type: string
                                enum:
                                - all_lp_details_on_call
                                description: Population the header money figures cover.
                                  Always every LP row on the call.
                  page:
                    type: integer
                  per_page:
                    type: integer
                  total:
                    type: integer
        '400':
          description: invalid parameter
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Entity not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/capital_calls/{id}":
    get:
      operationId: get_capital_call
      x-agent-tool: true
      summary: Show one capital call
      tags:
      - Capital Calls
      description: |
        Returns the header block for a single capital call. Use
        `/api/v1/capital_calls/{id}/details` for per-LP rows.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: Capital call id.
        required: true
        schema:
          type: integer
      responses:
        '200':
          description: capital call found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/CapitalCallHeader"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Capital call not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/capital_calls/{id}/details":
    get:
      operationId: list_capital_call_details
      x-agent-tool: true
      summary: List per-LP detail rows under a capital call
      tags:
      - Capital Calls
      description: |
        Returns the per-capital-account line items (LPs/GPs) attached to a
        capital call: amount called, the LP's committed capital, wired amount
        and wire date (from reconciled wire transactions on the period),
        variance (wired − called) and a derived payment_status
        (fully_paid/underpaid/overpaid/unpaid/prepaid/over_commitment/
        any_variance/unknown).
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Capital call id (plain integer).
        schema:
          type: integer
      - name: page
        in: query
        description: 1-indexed page number (default 1).
        required: false
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        description: Page size. Capped at 100; defaults to 50.
        required: false
        schema:
          type: integer
          default: 50
          maximum: 100
      responses:
        '200':
          description: paginated list of capital call details
          content:
            application/json:
              schema:
                type: object
                required:
                - details
                - page
                - per_page
                - total
                properties:
                  details:
                    type: array
                    items:
                      "$ref": "#/components/schemas/CapitalCallDetail"
                  page:
                    type: integer
                  per_page:
                    type: integer
                  total:
                    type: integer
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Capital call not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/financial_reports":
    get:
      operationId: list_financial_reports
      x-agent-tool: true
      summary: List financial report generation jobs
      tags:
      - Financial Reports
      description: Returns a paginated list of financial report generation jobs that
        belong to the authenticated API key's account, regardless of which API key
        within that account created them. Sorted by `created_at` descending. Successful
        jobs include download URLs for the generated files.
      security:
      - api_key: []
      parameters:
      - name: page
        in: query
        description: 0-indexed page number (50 jobs per page).
        schema:
          type: integer
          default: 0
      responses:
        '200':
          description: paginated list of jobs
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/financial_report_job"
                  pagination:
                    type: object
                    properties:
                      total_count:
                        type: integer
                      current_page:
                        type: integer
                      total_pages:
                        type: integer
        '401':
          description: Unauthorized
    post:
      operationId: generate_financial_report
      x-agent-tool: true
      summary: Generate a financial report
      tags:
      - Financial Reports
      description: Queues an asynchronous financial report generation job. The job
        includes every entity of the requested `entity_type` within the API key's
        account. Use the returned `status_url` to poll job status and download generated
        files after the job succeeds.
      security:
      - api_key: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                entity_type:
                  type: string
                  default: fund
                  description: Selects which class of account organization to include
                    in the report. The job covers every firm-admin-enabled entity
                    of this type within the authenticated API key's account.
                  enum:
                  - fund
                  - management_company
                  - general_partnership
                report_types:
                  type: array
                  description: Which report types to include in the generated file.
                    Each type becomes one worksheet (xlsx) or section (pdf). Required
                    unless `all_reports=true` is sent instead.
                  items:
                    type: string
                    enum:
                    - balance_sheet
                    - income_statement
                    - schedule_of_investments
                    - statement_of_changes
                all_reports:
                  type: boolean
                  default: false
                  description: Shortcut to include every supported report type. When
                    `true`, `report_types` may be omitted; when `false` or absent,
                    `report_types` is required.
                period:
                  type: string
                  default: last_financial_year
                  description: Reporting period for the generated report. Defaults
                    to `last_financial_year`. Pass `custom` together with `custom_start_date`
                    and `custom_end_date` to specify an explicit date range; for every
                    other value the dates are derived from the period name.
                  enum:
                  - this_month
                  - this_quarter
                  - this_financial_year
                  - last_month
                  - last_quarter
                  - last_financial_year
                  - month_to_date
                  - quarter_to_date
                  - year_to_date
                  - custom
                custom_start_date:
                  type: string
                  format: date
                  description: ISO 8601 date (YYYY-MM-DD). Required when `period=custom`;
                    ignored otherwise.
                custom_end_date:
                  type: string
                  format: date
                  description: ISO 8601 date (YYYY-MM-DD). Required when `period=custom`;
                    ignored otherwise. Must be on or after `custom_start_date`.
                format:
                  type: string
                  default: xlsx
                  description: Output format for the generated file. `xlsx` produces
                    a single workbook with one worksheet per report type plus supplemental
                    worksheets (PAA, General Ledger, Journal Entries). `pdf` produces
                    a single document with the same sections.
                  enum:
                  - xlsx
                  - pdf
              required:
              - report_types
            examples:
              balance_sheet:
                value:
                  report_types:
                  - balance_sheet
                  period: year_to_date
                  format: xlsx
      responses:
        '202':
          description: report generation job queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/financial_report_job"
        '400':
          description: Invalid parameter
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Account organization not found
        '422':
          description: Validation failed
  "/api/v1/financial_reports/{id}":
    get:
      operationId: get_financial_report
      x-agent-tool: true
      summary: Get financial report generation status
      tags:
      - Financial Reports
      description: Returns the current status for a financial report generation job
        created by the authenticated API key. Successful jobs include download URLs
        for the generated files.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: Financial report id.
        required: true
        schema:
          type: integer
      responses:
        '200':
          description: job status found
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/financial_report_job"
        '401':
          description: Unauthorized
        '404':
          description: Job not found
  "/api/v1/events":
    get:
      operationId: list_events
      x-agent-tool: true
      summary: List Events
      tags:
      - Events
      description: Gets a paginated list of Events in your account. The response will
        have a data array with the events and a pagination object with the total count,
        current page, and total pages. Pages are 0-indexed, so the first page is page
        0. The Events returned can be filtered and ordered. Order direction is ascending
        by default.
      security:
      - api_key: []
      parameters:
      - name: title
        in: query
        description: Event title includes this string (case-insensitive)
        schema:
          type: string
      - name: start_date
        in: query
        description: Only events starting on or after this date (ISO 8601 format)
        schema:
          type: string
      - name: end_date
        in: query
        description: Only events ending on or before this date (ISO 8601 format)
        schema:
          type: string
      - name: published
        in: query
        description: Filter by published status
        schema:
          type: boolean
      - name: access_type
        in: query
        description: Filter by access type
        schema:
          "$ref": "#/components/schemas/AccessType"
      - name: page
        in: query
        description: Page number (0-indexed)
        schema:
          default: 0
          type: integer
      - name: order_by
        in: query
        description: Field to order by
        schema:
          default: event_date
          enum:
          - title
          - event_date
          - created_at
          type: string
      - name: order_direction
        in: query
        description: Order direction
        schema:
          default: asc
          enum:
          - asc
          - desc
          type: string
      responses:
        '200':
          description: no errors
          content:
            application/json:
              examples:
                success:
                  value:
                    data:
                    - id: 123
                      unique_event_id: startup-pitch-night-2024
                      landing_page_header_text: Startup Pitch Night 2024
                      landing_page_super_text: Join us for an exciting evening of
                        innovation
                      event_when: '2024-03-15T18:00:00'
                      event_when_timezone: America/New_York
                      event_duration_minutes: 120
                      event_where_name: Innovation Hub
                      event_where_address: 123 Tech Street, San Francisco, CA
                      virtual_link:
                      attendance_limit: 100
                      access_type: public_access
                      published: true
                      shareable: true
                      waiting_list_enabled: false
                      rsvp_yes_count: 45
                      rsvp_no_count: 12
                      rsvp_waiting_list_count: 0
                      registration_url: https://your-domain.com/events/startup-pitch-night-2024
                      event_is_upcoming: true
                      event_is_past: false
                      seats_left: 55
                      created_at: '2024-02-01T10:00:00Z'
                      updated_at: '2024-02-15T14:30:00Z'
                    pagination:
                      total_count: 25
                      current_page: 0
                      total_pages: 1
        '401':
          description: Unauthorized
    post:
      operationId: create_event
      x-agent-tool: true
      summary: Create Event
      tags:
      - Events
      description: Creates a new Event in your account. A successful request will
        return the created event data with success status. The event will automatically
        have an associated pipeline and questionnaire created for guest management.
      security:
      - api_key: []
      parameters: []
      responses:
        '201':
          description: event created
          content:
            application/json:
              examples:
                created:
                  value:
                    success: true
                    data:
                      id: 124
                      unique_event_id: new-test-event
                      landing_page_header_text: New Test Event
                      registration_url: https://your-domain.com/events/new-test-event
                      created_at: '2024-02-20T10:00:00Z'
                    message: Event created successfully
        '400':
          description: bad request
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                invalid_timezone:
                  value:
                    error:
                      code: invalid_timezone
                      message: Invalid timezone 'Invalid/Timezone'
                      field: event_when_timezone
                      valid_values:
                      - America/New_York
                      - Etc/UTC
                validation_errors:
                  value:
                    error:
                      code: validation_failed
                      message: Event title can't be blank and Event date can't be
                        blank
                      details:
                      - Event title can't be blank
                      - Event date can't be blank
        '401':
          description: Unauthorized
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: object
                  properties:
                    landing_page_header_text:
                      type: string
                      description: Event title
                    landing_page_super_text:
                      type: string
                      description: Event subtitle
                    landing_page_body_text:
                      type: string
                      description: Event description
                    event_when:
                      type: string
                      format: date-time
                      description: Event start time (ISO 8601 format)
                    event_when_timezone:
                      "$ref": "#/components/schemas/TimeZone"
                      description: Event timezone (e.g., America/New_York)
                    event_duration_minutes:
                      type: integer
                      description: Event duration in minutes
                      default: 60
                    event_where_name:
                      type: string
                      description: Venue or location name
                    event_where_address:
                      type: string
                      description: Physical address for in-person events
                    virtual_link:
                      type: string
                      description: Virtual meeting link for online events
                    attendance_limit:
                      type: integer
                      description: Maximum number of attendees (0 for unlimited)
                      default: 0
                    access_type:
                      "$ref": "#/components/schemas/AccessType"
                      description: Event access type
                      default: public_access
                    published:
                      type: boolean
                      description: Whether the event is published
                      default: false
                    shareable:
                      type: boolean
                      description: Whether the event can be shared
                      default: true
                    waiting_list_enabled:
                      type: boolean
                      description: Whether to enable waiting list
                      default: false
                    phone_number_setting:
                      type: string
                      description: Phone number requirement setting
                      default: show_not_required
                    bottom_banner_header_text:
                      type: string
                      description: Bottom banner header text
                    bottom_banner_super_text:
                      type: string
                      description: Bottom banner subtitle text
                    hero_image_url:
                      type: string
                      description: Event image URL
                  required:
                  - landing_page_header_text
                  - event_when
                  - event_when_timezone
              required:
              - event
  "/api/v1/events/{id}":
    get:
      operationId: get_event
      x-agent-tool: true
      summary: Show Event
      tags:
      - Events
      description: 'Get a single Event by ID or unique_event_id. The default response
        shape is unchanged from prior versions. The optional include= and fields=
        parameters are purely opt-in - include= replaces the default include set (currently
        empty for events) with a caller-specified set, and fields= narrows top-level
        columns. include_guests=true is preserved as a backwards-compatible alias
        for include=guests.

        '
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Event ID or unique_event_id
        schema:
          type: string
      - name: include
        in: query
        description: |
          Comma-separated list of nested resources to embed in the response.
          Unknown values are ignored silently. Allowed values: guests.
        required: false
        schema:
          type: string
      - name: fields
        in: query
        description: |
          Comma-separated list of top-level fields to return. Unknown values
          are ignored silently. When `fields=` is supplied, the response is always narrowed; `id` and `name` are always present, and if no supplied keys match the whitelist the response collapses to just `{id, name}`.
        required: false
        schema:
          type: string
      - name: include_guests
        in: query
        description: Deprecated alias for include=guests; kept for backwards compatibility.
        schema:
          default: false
          type: boolean
      responses:
        '200':
          description: event found with guests
          content:
            application/json:
              examples:
                success:
                  value:
                    data:
                      id: 123
                      unique_event_id: show-test-event
                      landing_page_header_text: Show Test Event
                      landing_page_super_text: Join us for an exciting evening of
                        innovation
                      event_when: '2024-03-15T18:00:00'
                      event_when_timezone: America/New_York
                      event_duration_minutes: 120
                      event_where_name: Innovation Hub
                      event_where_address: 123 Tech Street, San Francisco, CA
                      virtual_link:
                      attendance_limit: 100
                      access_type: public_access
                      published: true
                      shareable: true
                      waiting_list_enabled: false
                      rsvp_yes_count: 45
                      rsvp_no_count: 12
                      rsvp_waiting_list_count: 0
                      registration_url: https://your-domain.com/events/show-test-event
                      event_is_upcoming: true
                      event_is_past: false
                      seats_left: 55
                      created_at: '2024-02-01T10:00:00Z'
                      updated_at: '2024-02-15T14:30:00Z'
                success_with_guests:
                  value:
                    data:
                      id: 123
                      unique_event_id: show-test-event
                      landing_page_header_text: Show Test Event
                      registration_url: https://your-domain.com/events/show-test-event
                      fund_event_rsvps:
                      - status: 'yes'
                        created_at: '2024-02-10T09:00:00Z'
                        updated_at: '2024-02-10T09:00:00Z'
                        person:
                          id: 456
                          email: john@example.com
                          first_name: John
                          last_name: Doe
                          phone: "+1234567890"
        '404':
          description: not found
        '401':
          description: Unauthorized
    put:
      operationId: update_event
      x-agent-tool: true
      summary: Update Event
      tags:
      - Events
      description: Update an existing Event. Only the provided fields will be updated.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Event ID or unique_event_id
        schema:
          type: string
      responses:
        '200':
          description: event updated
          content:
            application/json:
              examples:
                updated:
                  value:
                    success: true
                    data:
                      id: 124
                      landing_page_header_text: Updated Event Title
                      updated_at: '2024-02-20T11:00:00Z'
                    message: Event updated successfully
        '404':
          description: not found
        '401':
          description: Unauthorized
        '400':
          description: invalid timezone
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                invalid_timezone:
                  value:
                    error:
                      code: invalid_timezone
                      message: Invalid timezone 'Invalid/Timezone'
                      field: event_when_timezone
                      valid_values:
                      - America/New_York
                      - Etc/UTC
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: object
                  properties:
                    landing_page_header_text:
                      type: string
                      description: Event title
                    landing_page_super_text:
                      type: string
                      description: Event subtitle
                    landing_page_body_text:
                      type: string
                      description: Event description
                    event_when:
                      type: string
                      format: date-time
                      description: Event start time (ISO 8601 format)
                    event_when_timezone:
                      "$ref": "#/components/schemas/TimeZone"
                      description: Event timezone
                    event_duration_minutes:
                      type: integer
                      description: Event duration in minutes
                    event_where_name:
                      type: string
                      description: Venue or location name
                    event_where_address:
                      type: string
                      description: Physical address
                    virtual_link:
                      type: string
                      description: Virtual meeting link
                    attendance_limit:
                      type: integer
                      description: Maximum number of attendees
                    access_type:
                      "$ref": "#/components/schemas/AccessType"
                      description: Event access type
                    published:
                      type: boolean
                      description: Whether the event is published
                    shareable:
                      type: boolean
                      description: Whether the event can be shared
                    waiting_list_enabled:
                      type: boolean
                      description: Whether to enable waiting list
                    phone_number_setting:
                      type: string
                      description: Phone number requirement setting
                    bottom_banner_header_text:
                      type: string
                      description: Bottom banner header text
                    bottom_banner_super_text:
                      type: string
                      description: Bottom banner subtitle text
                    hero_image_url:
                      type: string
                      description: Event image URL
              required:
              - event
    delete:
      operationId: delete_event
      x-agent-tool: true
      summary: Delete Event
      tags:
      - Events
      description: Delete an Event and all associated data including pipeline and
        questionnaire.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Event ID or unique_event_id
        schema:
          type: string
      responses:
        '200':
          description: event deleted
          content:
            application/json:
              examples:
                deleted:
                  value:
                    success: true
                    message: Event deleted successfully
        '404':
          description: not found
        '401':
          description: Unauthorized
  "/api/v1/events/{id}/guests":
    post:
      operationId: add_event_guests
      x-agent-tool: true
      summary: Add Guests to Event
      tags:
      - Events
      description: Add multiple guests to an event. Guests will be created as People
        if they don't exist and added to the event pipeline. The response includes
        arrays of added, duplicate, and error results for processing feedback.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Event ID or unique_event_id
        schema:
          type: string
      responses:
        '200':
          description: guests added
          content:
            application/json:
              examples:
                guests_added:
                  value:
                    success: true
                    results:
                      added:
                      - john.doe@example.com
                      - jane.smith@example.com
                      duplicates: []
                      errors: []
                    message: Processed 2 guests
        '400':
          description: bad request
        '404':
          description: not found
        '401':
          description: Unauthorized
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: object
                  properties:
                    guests:
                      type: array
                      items:
                        type: object
                        properties:
                          first_name:
                            type: string
                            description: Guest first name
                          middle_name:
                            type: string
                            description: Guest middle name
                          last_name:
                            type: string
                            description: Guest last name
                          email:
                            type: string
                            description: Guest email address
                          phone:
                            type: string
                            description: Guest phone number
                          linkedin:
                            type: string
                            description: Guest LinkedIn URL
                          tag_list:
                            type: string
                            description: Comma-separated tags
                          address:
                            type: object
                            properties:
                              street:
                                type: string
                              city:
                                type: string
                              state:
                                type: string
                              country:
                                type: string
                              zipcode:
                                type: string
                          organizations:
                            type: array
                            items:
                              type: object
                              properties:
                                name:
                                  type: string
                                  description: Organization name
                                title:
                                  type: string
                                  description: Job title at organization
                        required:
                        - first_name
                        - last_name
                        - email
                  required:
                  - guests
              required:
              - event
    get:
      operationId: list_event_guests
      x-agent-tool: true
      summary: Get Event Guests
      tags:
      - Events
      description: Get a paginated list of guests for an event with their RSVP status
        and pipeline stage information. Results can be filtered by email or pipeline
        stage.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Event ID or unique_event_id
        schema:
          type: string
      - name: page
        in: query
        description: Page number (0-indexed)
        schema:
          default: 0
          type: integer
      - name: email
        in: query
        description: Filter by email (partial match)
        schema:
          type: string
      - name: stage
        in: query
        description: Filter by pipeline stage name
        schema:
          type: string
      responses:
        '200':
          description: guests retrieved
          content:
            application/json:
              examples:
                guests_list:
                  value:
                    data:
                    - id: 456
                      email: guest1@example.com
                      first_name: John
                      last_name: Doe
                      phone: "+1234567890"
                      stage: RSVP - Yes
                      rsvp_status: 'yes'
                      created_at: '2024-02-10T09:00:00Z'
                      updated_at: '2024-02-15T10:30:00Z'
                    - id: 457
                      email: guest2@example.com
                      first_name: Jane
                      last_name: Smith
                      phone: "+1987654321"
                      stage: RSVP - No
                      rsvp_status: 'no'
                      created_at: '2024-02-10T09:00:00Z'
                      updated_at: '2024-02-15T10:30:00Z'
                    pagination:
                      total_count: 2
                      current_page: 0
                      total_pages: 1
        '404':
          description: event not found
        '401':
          description: Unauthorized
  "/api/v1/events/{id}/rsvp":
    patch:
      operationId: update_event_rsvp
      x-agent-tool: true
      summary: Update Guest RSVP
      tags:
      - Events
      description: Update a guest's RSVP status for an event. This will also update
        the guest's pipeline stage accordingly.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Event ID or unique_event_id
        schema:
          type: string
      responses:
        '200':
          description: rsvp updated
          content:
            application/json:
              examples:
                rsvp_updated:
                  value:
                    success: true
                    data:
                      person_email: test@example.com
                      status: 'yes'
                      event_id: 123
                    message: RSVP updated successfully
        '400':
          description: bad request
        '404':
          description: event not found
        '401':
          description: Unauthorized
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                rsvp:
                  type: object
                  properties:
                    email:
                      type: string
                      description: Guest email address
                    status:
                      "$ref": "#/components/schemas/RsvpStatus"
                      description: RSVP status
                  required:
                  - email
                  - status
              required:
              - rsvp
  "/api/v1/files":
    get:
      operationId: list_files
      x-agent-tool: true
      summary: List Files
      tags:
      - Files
      description: Gets a paginated list of Files in your Files. The response will
        have a data array with the files and a pagination object with the total count,
        current page, and total pages. Pages are 0-indexed, so the first page is page
        0. The Files returned can be filtered and ordered. Order direction is ascending
        by default.
      security:
      - api_key: []
      parameters:
      - name: name
        in: query
        description: Name includes this string (case-insensitive)
        schema:
          type: string
      - name: folder_id
        in: query
        description: Filter by folder ID
        schema:
          type: integer
      - name: extension
        in: query
        description: Filter by file extension (e.g., .pdf, .doc)
        schema:
          type: string
      - name: created_before
        in: query
        description: Only files created on or before this date
        schema:
          type: string
      - name: created_after
        in: query
        description: Only files created on or after this date
        schema:
          type: string
      - name: page
        in: query
        description: Page number (0-indexed)
        schema:
          default: 0
          type: integer
      - name: order_by
        in: query
        description: "Field to order by:\n * `name` \n * `created_at` \n * `updated_at`
          \n "
        schema:
          default: created_at
          enum:
          - name
          - created_at
          - updated_at
          type: string
      - name: order_direction
        in: query
        description: Order direction
        schema:
          default: asc
          enum:
          - asc
          - desc
          type: string
      responses:
        '200':
          description: no errors
          content:
            application/json:
              examples:
                success:
                  value:
                    data:
                    - id: 1
                      name: Annual Report
                      description: Company annual report for 2024
                      created_at: '2024-08-06T18:32:52.335Z'
                      updated_at: '2024-08-06T18:32:52.335Z'
                      extension: ".pdf"
                      file_name: annual-report-2024.pdf
                      data_room_folders:
                      - id: 1
                        name: Financial Documents
                    - id: 2
                      name: Company Logo
                      description: Official company logo
                      created_at: '2024-08-06T18:32:52.335Z'
                      updated_at: '2024-08-06T18:32:52.335Z'
                      extension: ".png"
                      file_name: company-logo.png
                      data_room_folders:
                      - id: 2
                        name: Brand Assets
                    pagination:
                      total_count: 2
                      current_page: 0
                      total_pages: 1
        '401':
          description: Unauthorized
    post:
      operationId: upload_file
      x-agent-tool: true
      summary: Upload File
      tags:
      - Files
      description: |
        Uploads a new file.

        **Request format**

        Must be sent as `multipart/form-data` with file params **nested under
        the `file` key**:

        - `file[file]` — the upload (required)
        - `file[name]` — optional custom name
        - `file[description]` — optional description
        - `folder_id` — optional, **top-level** (not under `file[]`). If omitted,
          the file is placed in the account's default folder.
        - `file[attachable_type]` and `file[attachable_id]` — optional pair. When
          both are provided, the upload is *also* attached to the given record so
          it appears in that record's attachment list (in addition to landing in
          the data room). Allowed `attachable_type` values: `Person`,
          `Organization`, `PipelineProspect`. For `PipelineProspect`, the
          attachment is routed to the prospect's underlying `prospectable`
          (Person or Organization) — this matches UI behavior.

        **Supported extensions**

        `.ppt`, `.pptx`, `.key`, `.doc`, `.docx`, `.mov`, `.mp4`, `.ogg`, `.pdf`,
        `.png`, `.jpg`, `.jpeg`

        **Example**

        ```
        curl -X POST https://decilehub.com/api/v1/files \
          -H "Authorization: Bearer $TOKEN" \
          -F "file[file]=@./doc.pdf" \
          -F "file[name]=My Doc" \
          -F "folder_id=123"
        ```
      security:
      - api_key: []
      parameters: []
      responses:
        '201':
          description: file uploaded successfully
          content:
            application/json:
              examples:
                success:
                  value:
                    data:
                      id: 1
                      name: Test Document
                      description: A test document
                      created_at: '2024-08-06T18:32:52.335Z'
                      updated_at: '2024-08-06T18:32:52.335Z'
                      extension: ".pdf"
                      file_name: test.pdf
                      data_room_folders: []
                      attachment:
                    message: File uploaded successfully
                success_with_attachment:
                  value:
                    data:
                      id: 1
                      name: Meeting Transcript
                      description:
                      created_at: '2024-08-06T18:32:52.335Z'
                      updated_at: '2024-08-06T18:32:52.335Z'
                      extension: ".pdf"
                      file_name: transcript.pdf
                      data_room_folders: []
                      attachment:
                        id: 42
                        subject_type: Person
                        subject_id: 17
                        name: Meeting Transcript
                    message: File uploaded successfully
        '400':
          description: bad request
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                invalid_request_format:
                  value:
                    error:
                      code: bad_request
                      message: 'Invalid request format. Expected multipart/form-data
                        with nested params: file[file]=<upload>, optional file[name],
                        file[description], and top-level folder_id.'
                file_required:
                  value:
                    error:
                      code: bad_request
                      message: File is required
                      field: file
                invalid_file_type:
                  value:
                    error:
                      code: bad_request
                      message: 'Invalid file type. Allowed: .ppt, .pptx, .key, .doc,
                        .docx, .mov, .mp4, .ogg, .pdf, .png, .jpg, .jpeg'
                missing_parameter:
                  value:
                    error:
                      code: bad_request
                      message: 'Missing required parameter: file. Expected multipart/form-data
                        with file[file]=<upload>.'
                      field: file
        '401':
          description: Unauthorized
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file[file]:
                  type: string
                  format: binary
                  description: The file to upload (required)
                file[name]:
                  type: string
                  description: Custom name for the file (defaults to original filename)
                file[description]:
                  type: string
                  description: Optional description for the file
                file[attachable_type]:
                  type: string
                  enum:
                  - Person
                  - Organization
                  - PipelineProspect
                  description: Optional record type to attach this file to. Must be
                    supplied together with `file[attachable_id]`. When `PipelineProspect`,
                    the attachment is routed to the prospect's underlying Person/Organization
                    (matches UI behavior).
                file[attachable_id]:
                  type: integer
                  description: Optional id (in current account) of the record to attach
                    this file to. Must be supplied together with `file[attachable_type]`.
                folder_id:
                  type: integer
                  description: Optional folder to place the file in. Top-level field,
                    NOT nested under `file[]`. If omitted, uses the account's default
                    folder.
              required:
              - file[file]
  "/api/v1/files/{id}":
    get:
      operationId: get_file
      x-agent-tool: true
      summary: Show File
      tags:
      - Files
      description: Gets details for a specific file, including metadata and associated
        folders.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: File ID
        required: true
        schema:
          type: integer
      responses:
        '200':
          description: file found
          content:
            application/json:
              examples:
                success:
                  value:
                    data:
                      id: 1
                      name: Test Document
                      description: A test document for API documentation
                      created_at: '2024-08-06T18:32:52.335Z'
                      updated_at: '2024-08-06T18:32:52.335Z'
                      extension: ".pdf"
                      file_name: test-document.pdf
                      data_room_folders:
                      - id: 1
                        name: Test Folder
        '400':
          description: file not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                not_found:
                  value:
                    error:
                      code: bad_request
                      message: File not found
        '401':
          description: Unauthorized
    put:
      operationId: update_file
      x-agent-tool: true
      summary: Update File
      tags:
      - Files
      description: |
        Updates a file. You can update just the metadata (name/description),
        or upload a new version of the file — uploading a new file creates a new
        version while maintaining history.

        **Request format**

        Must be sent as `multipart/form-data` with params **nested under the
        `file` key**. All fields are optional:

        - `file[name]` — updated name
        - `file[description]` — updated description
        - `file[file]` — new file upload (creates a new version)
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: File ID
        required: true
        schema:
          type: integer
      responses:
        '200':
          description: new version created successfully
          content:
            application/json:
              examples:
                metadata_update:
                  value:
                    data:
                      id: 1
                      name: Updated Document
                      description: Updated description
                      created_at: '2024-08-06T18:32:52.335Z'
                      updated_at: '2024-08-06T18:32:52.335Z'
                      extension: ".pdf"
                      file_name: test-document.pdf
                      data_room_folders:
                      - id: 1
                        name: Test Folder
                    message: File updated successfully
                new_version:
                  value:
                    data:
                      id: 2
                      name: New Version
                      description: New version description
                      created_at: '2024-08-06T18:32:52.335Z'
                      updated_at: '2024-08-06T18:32:52.335Z'
                      extension: ".pdf"
                      file_name: test.pdf
                      data_room_folders:
                      - id: 1
                        name: Test Folder
                    message: New file version created successfully
        '404':
          description: file not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                not_found:
                  value:
                    error:
                      code: not_found
                      message: File not found
        '401':
          description: Unauthorized
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file[file]:
                  type: string
                  format: binary
                  description: New file to upload (creates new version)
                file[name]:
                  type: string
                  description: Updated name for the file
                file[description]:
                  type: string
                  description: Updated description for the file
  "/api/v1/files/{id}/download":
    get:
      operationId: download_file
      x-agent-tool: true
      summary: Download File
      tags:
      - Files
      description: Downloads a file from your Files. Returns the file content with
        appropriate headers for download.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: File ID
        required: true
        schema:
          type: integer
      responses:
        '200':
          description: file downloaded successfully
        '400':
          description: file not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                not_found:
                  value:
                    error:
                      code: bad_request
                      message: File not found
        '401':
          description: Unauthorized
  "/api/v1/files/{id}/save_to_folder":
    post:
      operationId: save_file_to_folder
      summary: Save File To Folder
      tags:
      - Files
      description: |
        Saves an in-message file (an Attachment) into a data room folder so it is
        durably stored and visible to the folder's audience.

        The `{id}` path parameter is the Attachment id. The target folder is given
        by the required top-level `folder_id` in the JSON body.

        The operation is idempotent on the pair of (file content, folder): if the
        same file content already exists in the folder, no new placement is created
        and the existing one is returned with `status: unchanged`. Otherwise a new
        placement is created and returned with `status: created`.

        The response includes `folder_path` (a breadcrumb such as "Parent > Child")
        and `audience` (a human readable phrase describing who can see the file),
        which the agent uses to compose its success message.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: Attachment ID
        required: true
        schema:
          type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                folder_id:
                  type: integer
                  description: ID of the data room folder to save the file into.
              required:
              - folder_id
      responses:
        '201':
          description: file saved to folder
          content:
            application/json:
              examples:
                created:
                  value:
                    folder_file_id: 101
                    status: created
                    folder_path: Financials > 2024
                    audience: This file will be visible to everyone in your account.
        '200':
          description: file already present in folder (idempotent)
          content:
            application/json:
              examples:
                unchanged:
                  value:
                    folder_file_id: 101
                    status: unchanged
                    folder_path: Financials > 2024
                    audience: This file will be visible to everyone in your account.
        '401':
          description: Unauthorized
        '403':
          description: forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: file or folder not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                not_found:
                  value:
                    error:
                      code: not_found
                      message: File or folder not found
        '400':
          description: missing folder_id or attachment has no file content
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                bad_request:
                  value:
                    error:
                      code: bad_request
                      message: 'Missing required parameter: folder_id.'
  "/api/v1/files/save_csv_to_folder":
    post:
      operationId: save_csv_to_folder
      x-agent-tool: true
      summary: Save CSV To Folder
      tags:
      - Files
      description: |
        Serializes a structured `{headers, rows}` payload into a CSV file
        server-side (Ruby CSV, correct quoting) and places it in a data room
        folder. Built for the agent "export this list as a CSV" flow: the agent
        passes rows it produced with other tools; no file bytes or base64 travel
        through the request.

        Every row must be exactly as long as `headers`, cells must be scalars
        (string, number, boolean, or null), and the whole payload is capped at
        5,000 cells including the header row. `filename` is sanitized (path
        segments stripped) and `.csv` is appended server-side.

        The response mirrors `save_to_folder` (`folder_path`, `audience`) and adds
        `file_name` plus `file_url`, a deep link to the saved file the agent can
        hand to the user.
      security:
      - api_key: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                filename:
                  type: string
                  description: Descriptive file name; ".csv" is appended and path
                    segments are stripped server-side.
                folder_id:
                  type: integer
                  description: ID of the data room folder to save the CSV into.
                csv:
                  type: object
                  properties:
                    headers:
                      type: array
                      items:
                        type: string
                    rows:
                      type: array
                      items:
                        type: array
                  required:
                  - headers
                  - rows
              required:
              - filename
              - folder_id
              - csv
      responses:
        '201':
          description: CSV generated and saved to folder
          content:
            application/json:
              examples:
                created:
                  value:
                    folder_file_id: 102
                    status: created
                    folder_path: Reports > Exports
                    audience: This file will be visible to everyone in your account.
                    file_name: lp-commitments.csv
                    file_url: https://acme.decilehub.com/data_room_v2/folder_files/abc123
        '401':
          description: Unauthorized
        '403':
          description: forbidden (folder not writable by this user)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: folder not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '400':
          description: invalid payload (missing params, ragged rows, non-scalar cells,
            or over the 5,000-cell cap)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                too_large:
                  value:
                    error:
                      code: bad_request
                      message: CSV payload exceeds the 5000-cell limit (got 5004 cells
                        including headers). Export a smaller subset.
  "/api/v1/personalized_links":
    post:
      operationId: create_personalized_link
      summary: Create Personalized Link
      tags:
      - Files
      description: |
        Generates a personalized data room preview link that grants a specific
        person or pipeline prospect direct access to a file or folder. External
        services use this to hand a recipient a ready-to-open link on behalf of a
        prospect: the returned `url` carries the recipient's access code, so
        opening it establishes their data room session without a login.

        Provide exactly one recipient (`person_id` or `pipeline_prospect_id`) and
        exactly one target (`folder_id` or `file_id`). When a `file_id` is given,
        the link resolves the file's current folder placement and deep-links to the
        file's preview within the personalized folder context.

        A prospect must resolve to a contact person (a Person prospect, or an
        Organization prospect with a primary contact); otherwise the request is
        rejected with `400 bad_request`.
      security:
      - api_key: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                person_id:
                  type: integer
                  description: ID of the person to personalize the link for. Provide
                    this or pipeline_prospect_id, not both.
                pipeline_prospect_id:
                  type: integer
                  description: ID of the pipeline prospect to personalize the link
                    for. Provide this or person_id, not both.
                folder_id:
                  type: integer
                  description: ID of the data room folder to link to. Provide this
                    or file_id, not both.
                file_id:
                  type: integer
                  description: ID of the data room file to link to. Provide this or
                    folder_id, not both.
      responses:
        '201':
          description: personalized link generated
          content:
            application/json:
              examples:
                folder_link:
                  value:
                    data:
                      url: https://acme.decilehub.com/data_room/preview/Xy7bQ2
                      person_id: 42
                      folder_id: 7
                file_link:
                  value:
                    data:
                      url: https://acme.decilehub.com/data_room/preview/Xy7bQ2?file=k3Jd9aLp
                      pipeline_prospect_id: 15
                      person_id: 42
                      file_id: 99
                      folder_id: 7
        '401':
          description: Unauthorized
        '403':
          description: forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: recipient or target not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                not_found:
                  value:
                    error:
                      code: not_found
                      message: Couldn't find Person
        '400':
          description: invalid recipient/target combination or unsupported prospect
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                bad_request:
                  value:
                    error:
                      code: bad_request
                      message: Provide exactly one of person_id or pipeline_prospect_id.
  "/api/v1/folders/search":
    get:
      operationId: search_data_room_folders
      summary: Search Data Room Folders
      tags:
      - Folders
      description: |
        Searches data room folders by name to help disambiguate a user's folder
        reference (for example "save it to Acme") into folder ids.

        Only folders in your account that you can read are returned. The match is
        case insensitive on the folder name and is capped at 50 results. Each match
        includes the folder id, name, a breadcrumb path (for example "Parent > Child"),
        and an audience phrase describing who can see files saved to that folder. The
        agent uses these to compose its confirmation request.

        The q parameter is required (a missing or blank q returns 400). A query
        that matches no folders returns an empty matches array.
      security:
      - api_key: []
      parameters:
      - name: q
        in: query
        description: Substring to match against folder names (case insensitive).
        required: true
        schema:
          type: string
      responses:
        '200':
          description: no errors
          content:
            application/json:
              examples:
                success:
                  value:
                    matches:
                    - folder_id: 1
                      name: Acme Financials
                      path: Portfolio > Acme Financials
                      audience: This file will be visible to everyone in your account.
                empty:
                  value:
                    matches: []
        '400':
          description: missing required parameter
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                bad_request:
                  value:
                    error:
                      code: bad_request
                      message: 'Missing required parameter: q.'
        '401':
          description: Unauthorized
  "/api/v1/folders":
    get:
      operationId: list_folders
      x-agent-tool: true
      summary: List Folders
      tags:
      - Files
      description: Gets a paginated list of Folders in your Files. The response will
        have a data array with the folders and a pagination object with the total
        count, current page, and total pages. Pages are 0-indexed, so the first page
        is page 0. The Folders returned can be filtered and ordered. Order direction
        is ascending by default.
      security:
      - api_key: []
      parameters:
      - name: name
        in: query
        description: Name includes this string (case-insensitive)
        schema:
          type: string
      - name: folder_type
        in: query
        description: Filter by folder type
        schema:
          type: string
          enum:
          - standard
          - deal_memo_export
          - firm
          - fund_lp_documents
          - directory
          - venture_partner
      - name: parent_id
        in: query
        description: Filter by parent folder ID
        schema:
          type: integer
      - name: created_before
        in: query
        description: Only folders created on or before this date
        schema:
          type: string
      - name: created_after
        in: query
        description: Only folders created on or after this date
        schema:
          type: string
      - name: page
        in: query
        description: Page number (0-indexed)
        schema:
          default: 0
          type: integer
      - name: order_by
        in: query
        description: "Field to order by:\n * `name` \n * `created_at` \n * `updated_at`
          \n "
        schema:
          default: created_at
          enum:
          - name
          - created_at
          - updated_at
          type: string
      - name: order_direction
        in: query
        description: Order direction
        schema:
          default: asc
          enum:
          - asc
          - desc
          type: string
      responses:
        '200':
          description: no errors
          content:
            application/json:
              examples:
                success:
                  value:
                    data:
                    - id: 1
                      name: Root Folder
                      description: A root folder for organizing files
                      folder_type: firm
                      created_at: '2024-08-06T18:32:52.335Z'
                      updated_at: '2024-08-06T18:32:52.335Z'
                      shareable?: true
                      parent:
                      root_parent:
                    - id: 2
                      name: Child Folder
                      description: A child folder
                      folder_type: firm
                      created_at: '2024-08-06T18:32:52.335Z'
                      updated_at: '2024-08-06T18:32:52.335Z'
                      shareable?: false
                      parent:
                        id: 1
                        name: Root Folder
                      root_parent:
                        id: 1
                        name: Root Folder
                    pagination:
                      total_count: 2
                      current_page: 0
                      total_pages: 1
        '401':
          description: Unauthorized
    post:
      operationId: create_folder
      x-agent-tool: true
      summary: Create Folder
      tags:
      - Files
      description: Creates a new folder in your Files. You can optionally specify
        a parent folder by providing a parent_id. Folder names must be unique within
        the same parent folder.
      security:
      - api_key: []
      parameters: []
      responses:
        '201':
          description: child folder created successfully
          content:
            application/json:
              examples:
                success:
                  value:
                    data:
                      id: 1
                      name: New Test Folder
                      description:
                      folder_type: standard
                      created_at: '2024-08-06T18:32:52.335Z'
                      updated_at: '2024-08-06T18:32:52.335Z'
                      shareable?: true
                      parent:
                      root_parent:
                    message: Folder created successfully
                child_folder:
                  value:
                    data:
                      id: 2
                      name: Child Folder
                      description:
                      folder_type: standard
                      created_at: '2024-08-06T18:32:52.335Z'
                      updated_at: '2024-08-06T18:32:52.335Z'
                      shareable?: true
                      parent:
                        id: 1
                        name: Parent Folder
                      root_parent:
                        id: 1
                        name: Parent Folder
                    message: Folder created successfully
        '400':
          description: malformed request
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                missing_name:
                  value:
                    error:
                      code: bad_request
                      message: Folder name is required
                      field: name
                duplicate_name:
                  value:
                    error:
                      code: validation_failed
                      message: Name has already been taken
                      details:
                      - Name has already been taken
                malformed_request:
                  value:
                    error:
                      code: bad_request
                      message: 'param is missing or the value is empty or invalid:
                        folder'
                      field: folder
        '404':
          description: parent folder not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                parent_not_found:
                  value:
                    error:
                      code: not_found
                      message: Parent folder not found
        '401':
          description: Unauthorized
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                folder:
                  type: object
                  properties:
                    name:
                      type: string
                      description: The name of the folder
                    parent_id:
                      type: integer
                      description: Optional parent folder ID
                  required:
                  - name
              required:
              - folder
  "/api/v1/folders/{id}":
    get:
      operationId: get_folder
      x-agent-tool: true
      summary: Show Folder
      tags:
      - Files
      description: 'Gets details for a specific folder, including optional child folders
        and files. Use the contents parameter to specify what to include: "folders",
        "files", or "folders,files". Default includes both folders and files.'
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: Folder ID
        required: true
        schema:
          type: integer
      - name: contents
        in: query
        description: 'What to include: "folders", "files", "folders,files", or "none"'
        schema:
          default: folders,files
          type: string
      - name: name
        in: query
        description: Filter child items by name (case-insensitive)
        schema:
          type: string
      - name: order_by
        in: query
        description: "Field to order child items by:\n * `name` \n * `created_at`
          \n * `updated_at` \n "
        schema:
          default: created_at
          enum:
          - name
          - created_at
          - updated_at
          type: string
      - name: order_direction
        in: query
        description: Order direction for child items
        schema:
          default: asc
          enum:
          - asc
          - desc
          type: string
      responses:
        '200':
          description: folder found
          content:
            application/json:
              examples:
                success:
                  value:
                    data:
                      id: 1
                      name: Test Folder
                      description: A test folder
                      folder_type: firm
                      created_at: '2024-08-06T18:32:52.335Z'
                      updated_at: '2024-08-06T18:32:52.335Z'
                      shareable?: true
                      parent:
                      root_parent:
                    children:
                      folders:
                      - id: 2
                        name: Child Folder
                        description: A child folder
                        folder_type: firm
                        created_at: '2024-08-06T18:32:52.335Z'
                        updated_at: '2024-08-06T18:32:52.335Z'
                        shareable?: false
                        parent:
                          id: 1
                          name: Test Folder
                        root_parent:
                          id: 1
                          name: Test Folder
                      folders_count: 1
                      folders_displayed: 1
                      files:
                      - id: 1
                        created_at: '2024-08-06T18:32:52.335Z'
                        updated_at: '2024-08-06T18:32:52.335Z'
                        data_room_file:
                          id: 1
                          name: Test Document
                          description: A test document
                          created_at: '2024-08-06T18:32:52.335Z'
                          updated_at: '2024-08-06T18:32:52.335Z'
                          extension: ".pdf"
                          file_name: test-document.pdf
                      files_count: 1
                      files_displayed: 1
        '404':
          description: folder not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                not_found:
                  value:
                    error:
                      code: not_found
                      message: Folder not found
        '401':
          description: Unauthorized
  "/api/v1/organizations":
    get:
      operationId: list_organizations
      x-agent-tool: true
      summary: List Organizations
      tags:
      - Directory
      description: Gets a paginated list of Organizations in your Organizations Directory.
        The response will have a data array with the organizations and a pagination
        object with the total count, current page, and total pages.Pages are 0-indexed,
        so the first page is page 0.The Organizations returned can be filtered and
        ordered. Order direction is ascending by default. The default response shape
        is unchanged from prior versions and embeds notes, people, and submit_your_company_response_data.
        The optional include= parameter is purely opt-in - when supplied it replaces
        the default include set with a caller-specified subset; absent the parameter,
        the legacy default is returned verbatim. fields= narrows top-level columns
        when supplied.
      security:
      - api_key: []
      parameters:
      - name: include
        in: query
        description: |
          Comma-separated list of nested resources to embed. Unknown values
          are ignored silently. Allowed values: notes, people, referred_by,
          submit_your_company_response_data.
        required: false
        schema:
          type: string
      - name: fields
        in: query
        description: |
          Comma-separated list of top-level fields to return. Unknown values
          are ignored silently. When `fields=` is supplied, the response is always narrowed; `id` and `name` are always present, and if no supplied keys match the whitelist the response collapses to just `{id, name}`.
        required: false
        schema:
          type: string
      - name: custom_data_points
        in: query
        description: |
          Optional. Controls embedding of a custom_data_points field on
          each organization.

          - Omit the param entirely to leave custom_data_points out of the
            response (default).
          - Pass `*` to embed every account-declared organization_data_points
            key (subject to the allow-list).
          - Pass a comma-separated list (e.g. `key1,key2`) to narrow the
            embed to that subset. Unknown keys are silently dropped.
          - Pass an empty value to embed an explicit empty
            custom_data_points object.
          - The wildcard always wins, so `*,foo` behaves identically to
            `*`.

          Select-type values are resolved to their human-readable option
          labels rather than the underlying stored value. Internal jsonb
          keys are never returned.
        required: false
        schema:
          type: string
      - name: name
        in: query
        description: Name includes this string (case-insensitive)
        schema:
          type: string
      - name: created_before
        in: query
        description: Only organizations created on or before this date
        schema:
          type: string
      - name: created_after
        in: query
        description: Only organizations created on or after this date
        schema:
          type: string
      - name: page
        in: query
        description: Page number (0-indexed)
        schema:
          default: 0
          type: integer
      - name: order_by
        in: query
        description: "Field to order by:\n * `first_name` \n * `last_name` \n * `email`
          \n * `created_at` \n "
        schema:
          default: created_at
          enum:
          - first_name
          - last_name
          - email
          - created_at
          type: string
      - name: order_direction
        in: query
        description: Order direction
        schema:
          default: asc
          enum:
          - asc
          - desc
          type: string
      responses:
        '200':
          description: no errors
          content:
            application/json:
              examples:
                success:
                  value:
                    data:
                    - id: 13
                      address:
                        city: ''
                        state: ''
                        street: ''
                        country: ''
                        zipcode: ''
                      name: Some Org
                      company_url: https://www.some-organization-site.com
                      short_description: Some short description
                      traction: "$2M ARR, 5k active users, 20% MoM growth"
                      data:
                        custom_field: custom value
                        another field: another value
                      referred_by:
                        id: 54
                        email: florene.harvey.18@kirlinincllc.com
                        phone: "+918727727698"
                        first_name: Florene
                        middle_name:
                        last_name: Harvey
                      notes:
                      - body: This is a note
                        created_at: '2024-08-22T14:13:06.672Z'
                        author:
                          first_name: Apple
                          last_name: Owner
                      - body: This is another note
                        created_at: '2024-08-22T14:13:11.320Z'
                        author:
                          first_name: Apple
                          last_name: Owner
                      people:
                      - id: 105
                        email: jack@mcglonch.io
                        phone: '1112223344'
                        first_name: Jack
                        middle_name:
                        last_name: McGloncho
                      submit_your_company_response_data:
                        submitted_at: '2024-08-22T14:13:06.672Z'
                        responses:
                          what_problem_are_you_solving: Climate change solutions through
                            AI technology
                          describe_your_product: AI-powered carbon tracking platform
                    pagination:
                      total_count: 1
                      current_page: 0
                      total_pages: 1
    patch:
      operationId: update_organizations
      x-agent-tool: true
      summary: Update Organizations by ID
      tags:
      - Directory
      description: Updates up to 100 organizations by account-scoped ID and returns
        per-item updated and errors arrays. Requests with more than 100 items are
        rejected before any update runs. Remote logo URLs are fetched sequentially;
        omit logos or split image-heavy updates into smaller batches.
      security:
      - api_key: []
      responses:
        '200':
          description: Batch update results, including partial success or an empty
            request.
        '400':
          description: Missing or malformed organizations parameter, or more than
            100 items.
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                organizations:
                  type: array
                  maxItems: 100
                  items:
                    allOf:
                    - type: object
                      properties:
                        id:
                          type: string
                          pattern: "^\\d+$"
                      required:
                      - id
                    - "$ref": "#/components/schemas/organization_update_attributes"
              required:
              - organizations
    post:
      operationId: create_organizations
      x-agent-tool: true
      summary: Create Organizations
      tags:
      - Directory
      description: Adds Organizations to your Organizations Directory. Only the first
        100 Organizations per request will be processed.The response will have a created,
        duplicates, and errors array. The created array will contain the emails of
        the organizations that were created. The duplicates array will contain the
        emails of the organizations that were not created because they already exist
        in the database. The errors array will contain the emails of the organizations
        that were not created because of an error.
      security:
      - api_key: []
      parameters: []
      responses:
        '200':
          description: no errors or creations
          content:
            application/json:
              examples:
                no_creates:
                  value:
                    created: []
                    duplicates:
                    - Test Org
                    errors: []
        '201':
          description: organizations created
          content:
            application/json:
              examples:
                at_least_one_created:
                  value:
                    created:
                    - Test Org
                    duplicates: []
                    errors: []
        '400':
          description: bad request
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                invalid_order_by:
                  value:
                    error:
                      code: invalid_parameter
                      message: 'order_by must be one of: title, event_date, created_at'
                      field: order_by
                      valid_values:
                      - title
                      - event_date
                      - created_at
        '401':
          description: Unauthorized
        '422':
          description: unprocessable entity
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                no_creates_and_has_errors:
                  value:
                    error:
                      code: validation_failed
                      message: Some organizations could not be created
                      details:
                      - ": Validation failed: Name can't be blank"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                organizations:
                  type: array
                  items:
                    type: object
                    properties:
                      name:
                        type: string
                      website:
                        type: string
                      description:
                        type: string
                      tag_list:
                        type: string
                        description: Comma-separated tags to add.
                      logo:
                        type: string
                        description: Logo, as a base64 data URI or a public http(s)
                          image URL. JPG, PNG, WebP, or GIF; max 5 MB.
                      custom_data_points:
                        type: object
                      note:
                        type: string
                      traction:
                        type: string
                        description: Freetext traction summary (revenue, users, growth,
                          etc.).
                      address:
                        "$ref": "#/components/schemas/address"
                      referred_by:
                        description: Referrer as a person object. Matched or created
                          by email — not an ID.
                        "$ref": "#/components/schemas/associated_person"
                      people:
                        type: array
                        items:
                          "$ref": "#/components/schemas/associated_person"
                    required:
                    - name
              required:
              - organizations
  "/api/v1/tasks":
    get:
      operationId: list_tasks
      x-agent-tool: true
      summary: List Tasks
      tags:
      - Tasks
      description: Returns a paginated, account-scoped list of Tasks. Optional filters
        narrow by status, assignee, and origin. Pages are 0-indexed.
      security:
      - api_key: []
      parameters:
      - name: status
        in: query
        description: Filter by lifecycle status
        required: false
        schema:
          type: string
          enum:
          - open
          - in_progress
          - completed
          - cancelled
      - name: assignee_id
        in: query
        description: Filter by assignee user id
        required: false
        schema:
          type: integer
      - name: origin
        in: query
        description: Filter by task origin
        required: false
        schema:
          type: string
          enum:
          - agent_suspension
          - user
          - system
      - name: page
        in: query
        description: Page number (0-indexed)
        required: false
        schema:
          type: integer
      responses:
        '200':
          description: tasks listed
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Task"
                  pagination:
                    "$ref": "#/components/schemas/pagination"
        '401':
          description: Unauthorized
    post:
      operationId: create_task
      x-agent-tool: true
      summary: Create Task
      tags:
      - Tasks
      description: Creates a user-origin Task assigned to a member of the account.
        The authenticated API key's user is recorded as the creator.
      security:
      - api_key: []
      parameters: []
      responses:
        '201':
          description: task created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/Task"
        '401':
          description: Unauthorized
        '404':
          description: assignee is not an account member
        '422':
          description: validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                task:
                  type: object
                  properties:
                    title:
                      type: string
                    description:
                      type: string
                    assigner_note:
                      type: string
                      description: Optional free-text note from the assigner, rendered
                        prominently to the assignee on the task page.
                    assignee_id:
                      type: integer
                    due_date:
                      type: string
                      format: date
                    action_type:
                      type: string
                      description: In-place affordance the task asks for (defaults
                        to open_link).
                      enum:
                      - open_link
                      - review_approve
                      - upload
                      - sign
                      - fill_form
                      - verify_identity
                      - make_payment
                      - hub_action
                      - provide_info
                      - follow_up
                      - schedule
                      - provide_tax_info
                    action_config:
                      type: object
                      description: 'Per-action-type config (shallow; unknown keys
                        dropped). review_approve: review_url. upload: min_files, target_folder_id.
                        sign: sign_url (http, required), provider_label. fill_form:
                        form_url (http, required), form_label. make_payment: pay_url
                        (http, required), payee, amount_dollars or amount_cents. verify_identity:
                        verification_url, requires_approval, auto_send, email_body.
                        hub_action: screen_label. provide_info/follow_up/schedule:
                        artifact, deadline. provide_tax_info: artifact (w9|w8|k1|ein),
                        deadline — and requires a FirmAdmin::CapitalAccount subject.'
                    deep_link_url:
                      type: string
                      description: Optional primary-action URL (internal Hub path
                        or http(s) URL).
                    subject_type:
                      type: string
                      description: Optional polymorphic subject type. Must be paired
                        with subject_id and live in the account.
                      enum:
                      - Person
                      - People::UnifiedRegistry
                      - FirmAdmin::CapitalAccountContact
                      - AccountOrganization
                      - FirmAdmin::CapitalAccount
                      - FirmAdmin::CapitalCall
                    subject_id:
                      type: integer
                      description: Id of the subject record (paired with subject_type).
                  required:
                  - title
                  - assignee_id
              required:
              - task
  "/api/v1/tasks/{id}":
    get:
      operationId: get_task
      x-agent-tool: true
      summary: Get Task
      tags:
      - Tasks
      description: Returns a single account-scoped Task by id.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: Task id.
        required: true
        schema:
          type: integer
      responses:
        '200':
          description: task found
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/Task"
        '401':
          description: Unauthorized
        '404':
          description: task not found
    patch:
      operationId: update_task
      x-agent-tool: true
      summary: Update Task
      tags:
      - Tasks
      description: Updates a Task's title, description, assignee, due date, or status.
        Permitted for the task creator, current assignee, account owners/admins, and
        Decile admins.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: Task id.
        required: true
        schema:
          type: integer
      responses:
        '200':
          description: task updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/Task"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: task not found, or assignee not a member
        '422':
          description: validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                task:
                  type: object
                  properties:
                    title:
                      type: string
                    description:
                      type: string
                    assignee_id:
                      type: integer
                    due_date:
                      type: string
                      format: date
                    status:
                      type: string
                      enum:
                      - open
                      - in_progress
                      - completed
                      - cancelled
              required:
              - task
  "/api/v1/tasks/{id}/complete":
    post:
      operationId: complete_task
      x-agent-tool: true
      summary: Complete Task
      tags:
      - Tasks
      description: Marks a Task as completed. Permitted for the task creator, current
        assignee, account owners/admins, and Decile admins.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: Task id.
        required: true
        schema:
          type: integer
      responses:
        '200':
          description: task completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/Task"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: task not found
  "/api/v1/tasks/{id}/uploads":
    post:
      operationId: upload_task_document
      summary: Upload Task Document
      tags:
      - Tasks
      description: |
        Attaches one or more uploaded files to a Task. Only open upload tasks
        and provide-tax-info document tasks (W-9/W-8/K-1) accept files; tasks in
        a terminal status are rejected. A provide-tax-info document task
        completes automatically once its required documents are received. An
        upload task stays open — call Complete Task once it has at least its
        required number of files (`min_files`). Permitted for the task assignee
        and Decile admins.

        Must be sent as `multipart/form-data`. Send a single file as a `file`
        part, or several files as repeated `file[]` parts — repeating an
        unbracketed `file` part keeps only the last file. File size is limited
        by the account's data room upload limit.

        **Example**

        ```
        curl -X POST https://decilehub.com/api/v1/tasks/42/uploads \
          -H "Authorization: Bearer $TOKEN" \
          -F "file[]=@./w9.pdf" \
          -F "file[]=@./w8.pdf"
        ```
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: Task id.
        required: true
        schema:
          type: integer
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: The file to attach (use repeated `file[]` parts to
                    attach several)
              required:
              - file
      responses:
        '201':
          description: file(s) attached
          content:
            application/json:
              schema:
                type: object
                properties:
                  task_id:
                    type: integer
                  documents_count:
                    type: integer
                  attached:
                    type: array
                    items:
                      type: object
                      properties:
                        blob_id:
                          type: integer
                        filename:
                          type: string
              examples:
                success:
                  value:
                    task_id: 42
                    documents_count: 1
                    attached:
                    - blob_id: 7
                      filename: w9.pdf
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: task not found
        '422':
          description: task does not accept uploads, file missing, or file too large
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/tasks/{id}/search_files":
    get:
      operationId: search_task_linkable_files
      x-agent-tool: true
      summary: Search files linkable onto an upload Task
      tags:
      - Tasks
      description: Returns up to 20 recent data room files (newest first) the caller
        may read in the task's account, optionally filtered by file name. Only the
        task assignee (or a Decile admin) may search. Link a result with link_file
        as an alternative to uploading a new document.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: Task id.
        required: true
        schema:
          type: integer
      - name: query
        in: query
        description: Case-insensitive file name filter.
        required: false
        schema:
          type: string
      responses:
        '200':
          description: linkable files listed
          content:
            application/json:
              schema:
                type: object
                properties:
                  files:
                    type: array
                    items:
                      type: object
                      properties:
                        folder_file_id:
                          type: integer
                        name:
                          type: string
                        folder_name:
                          type: string
                          nullable: true
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: task not found
  "/api/v1/tasks/{id}/link_file":
    post:
      operationId: link_task_file
      x-agent-tool: true
      summary: Link a data room file onto an upload Task
      tags:
      - Tasks
      description: Links an existing data room file onto an open upload Task (idempotent
        — re-linking the same file succeeds without duplicating). Linked files count
        toward the task's required file minimum (min_files) alongside uploaded documents,
        so a task needing several files can mix uploads and links. Only the task assignee
        (or a Decile admin) may link.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: Task id.
        required: true
        schema:
          type: integer
      responses:
        '201':
          description: file linked
          content:
            application/json:
              schema:
                type: object
                properties:
                  task_id:
                    type: integer
                  folder_file_id:
                    type: integer
                  documents_and_links_count:
                    type: integer
                    description: Uploaded documents plus linked files, compared against
                      the task's min_files.
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: task or readable data room file not found
        '422':
          description: task is not an open upload task
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                data_room_folder_file_id:
                  type: integer
                  description: Data room folder file id (from search_files).
              required:
              - data_room_folder_file_id
  "/api/v1/tasks/{id}/unlink_file":
    delete:
      operationId: unlink_task_file
      x-agent-tool: true
      summary: Unlink a data room file from an upload Task
      tags:
      - Tasks
      description: Removes a previously linked data room file from an open upload
        Task (idempotent — unlinking a file that is not linked succeeds). Only the
        task assignee (or a Decile admin) may unlink.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: Task id.
        required: true
        schema:
          type: integer
      - name: data_room_folder_file_id
        in: query
        description: Data room folder file id to unlink.
        required: true
        schema:
          type: integer
      responses:
        '200':
          description: file unlinked
          content:
            application/json:
              schema:
                type: object
                properties:
                  task_id:
                    type: integer
                  folder_file_id:
                    type: integer
                  documents_and_links_count:
                    type: integer
                    description: Uploaded documents plus linked files, compared against
                      the task's min_files.
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: task not found
        '422':
          description: task is not an open upload task, or data_room_folder_file_id
            is missing
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/tasks/{task_id}/decision":
    post:
      operationId: submit_task_decision
      x-agent-tool: true
      summary: Submit a non-compliant-wire compliance-review decision
      tags:
      - Tasks
      description: |
        Applies only to a non-compliant-wire compliance-review Task created by
        that workflow. Review the supporting evidence and task context first
        before deciding. A decline requires notes (422 task_decision_notes_required
        otherwise) and returns the wire to the assigned VA for rework; the
        declined task remains in progress for revision and can later be
        approved, appending an approval. The original audit remains immutable.
        An identical retry is idempotent only when the latest decision and
        normalized notes match (nil and blank both mean no note) AND no new
        document has been collected since — a decline over a freshly
        re-collected document is a new decision even with identical notes.
        Any other different decision, including an opposing request based on
        stale pre-decline state, or changed notes returns 409.
      security:
      - api_key: []
      parameters:
      - name: task_id
        in: path
        description: Task id.
        required: true
        schema:
          type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/TaskDecisionRequest"
      responses:
        '200':
          description: Decision recorded or already recorded identically
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/TaskDecisionResponse"
        '400':
          description: Invalid decision
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden, including read-only keys or unauthorized actors
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Task not found in the current account
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '409':
          description: Any other different decision, including an opposing request
            based on stale pre-decline state, or changed notes conflict with the immutable
            recorded audit
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: Confirmation required or Task is not decidable
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/organization":
    post:
      operationId: upsert_organization
      x-agent-tool: true
      summary: Create/Update an Organization
      tags:
      - Directory
      description: 'Create or Update an existing Organization. The request can only
        contain one Organization. A successful request will return a json object containing
        the success status, the organization_id, and a list of changes in the form
        of field_name: [old_value, new_value].'
      security:
      - api_key: []
      parameters: []
      responses:
        '201':
          description: organization created/updated
          content:
            application/json:
              examples:
                created:
                  value:
                    status: success
                    organization_id: 1
                    changes:
                      name:
                      -
                      - Jane Co.
                      company_url:
                      -
                      - https://www.example.com
                      short_description:
                      -
                      - An example org
        '400':
          description: bad request
        '401':
          description: Unauthorized
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                organization:
                  type: object
                  properties:
                    name:
                      type: string
                    website:
                      type: string
                    description:
                      type: string
                    logo:
                      type: string
                      description: Logo, as a base64 data URI (data:image/png;base64,...)
                        or a public http(s) image URL. JPG, PNG, WebP, or GIF; max
                        5 MB.
                    tag_list:
                      type: string
                      description: Comma-separated tags to add.
                    remove_tag_list:
                      type: string
                      description: Comma-separated tags to remove. Tags not present
                        are ignored. A record is not created solely to remove tags.
                    custom_data_points:
                      type: object
                    note:
                      type: string
                    traction:
                      type: string
                      description: Freetext traction summary (revenue, users, growth,
                        etc.).
                    address:
                      "$ref": "#/components/schemas/address"
                    referred_by:
                      description: Referrer as a person object. Matched or created
                        by email — not an ID.
                      "$ref": "#/components/schemas/associated_person"
                    people:
                      type: array
                      items:
                        "$ref": "#/components/schemas/associated_person"
                  required:
                  - name
              required:
              - organization
  "/api/v1/organizations/{id}":
    get:
      operationId: get_organization
      x-agent-tool: true
      summary: Show Organization
      tags:
      - Directory
      description: 'Get a single Organization by id. The default response shape is
        unchanged from prior versions (embeds notes, people, submit_your_company_response_data).
        The optional include= and fields= parameters are purely opt-in - include=
        replaces the default include set with a caller-specified subset, and fields=
        narrows top-level columns. The response also carries a `logo` object (see
        schemas/attached_image) with a download `url`, or null when no logo is attached.

        '
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Organization ID
        schema:
          type: integer
      - name: include
        in: query
        description: |
          Comma-separated list of nested resources to embed. Unknown values
          are ignored silently. Allowed values: notes, people, referred_by,
          submit_your_company_response_data.
        required: false
        schema:
          type: string
      - name: fields
        in: query
        description: |
          Comma-separated list of top-level fields to return. Unknown values
          are ignored silently. When `fields=` is supplied, the response is always narrowed; `id` and `name` are always present, and if no supplied keys match the whitelist the response collapses to just `{id, name}`.
        required: false
        schema:
          type: string
      - name: custom_data_points
        in: query
        description: |
          Optional. Controls embedding of a custom_data_points field on
          the organization.

          - Omit the param entirely to leave custom_data_points out of the
            response (default).
          - Pass `*` to embed every account-declared organization_data_points
            key (subject to the allow-list).
          - Pass a comma-separated list (e.g. `key1,key2`) to narrow the
            embed to that subset. Unknown keys are silently dropped.
          - Pass an empty value to embed an explicit empty
            custom_data_points object.
          - The wildcard always wins, so `*,foo` behaves identically to
            `*`.

          Select-type values are resolved to their human-readable option
          labels rather than the underlying stored value. Internal jsonb
          keys are never returned.
        required: false
        schema:
          type: string
      responses:
        '200':
          description: organization found
          content:
            application/json:
              examples:
                success:
                  value:
                    data:
                      id: 1
                      address:
                        city: ''
                        state: ''
                        street: ''
                        country: ''
                        zipcode: ''
                      name: ShowOrg
                      company_url: ''
                      short_description: ''
                      traction: "$2M ARR, 5k active users, 20% MoM growth"
                      data: {}
                      referred_by:
                      notes: []
                      people: []
                      submit_your_company_response_data:
        '404':
          description: not found
        '401':
          description: Unauthorized
    patch:
      operationId: update_organization
      x-agent-tool: true
      summary: Update Organization by ID
      tags:
      - Directory
      description: Updates the existing organization selected by account-scoped ID;
        changing its name does not create a record.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Organization ID
        schema:
          type: string
          pattern: "^\\d+$"
      responses:
        '200':
          description: Organization updated
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Organization not found
        '422':
          description: Validation failed
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                organization:
                  "$ref": "#/components/schemas/organization_update_attributes"
              required:
              - organization
  "/api/v1/organizations/{id}/notes":
    post:
      operationId: add_organization_note
      x-agent-tool: true
      summary: Add a note to an organization
      tags:
      - Directory
      description: |
        Append a plain-text note to an organization. The note is stored on the organization
        and surfaced wherever organization notes appear (including the prospect view if the
        organization is a pipeline prospect's prospectable).
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Organization ID
        schema:
          type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                note:
                  type: object
                  properties:
                    body:
                      type: string
                    context:
                      type: string
                  required:
                  - body
              required:
              - note
      responses:
        '201':
          description: note created
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/note_envelope"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Organization not found
        '422':
          description: Validation error
  "/api/v1/people":
    get:
      operationId: list_people
      x-agent-tool: true
      summary: List People
      tags:
      - Directory
      description: Gets a paginated list of People in your People Directory. The response
        will have a data array with the people and a pagination object with the total
        count, current page, and total pages.Pages are 0-indexed, so the first page
        is page 0.The People returned can be filtered and ordered. Order direction
        is ascending by default. The default response shape is unchanged from prior
        versions and embeds notes, organizations_with_titles, and referred_by. The
        optional include= parameter is purely opt-in - when supplied it replaces the
        default include set with a caller-specified subset; absent the parameter,
        the legacy default is returned verbatim. fields= narrows top-level columns
        when supplied.
      security:
      - api_key: []
      parameters:
      - name: include
        in: query
        description: |
          Comma-separated list of nested resources to embed. Unknown values
          are ignored silently. Allowed values: notes, referred_by, organizations.
        required: false
        schema:
          type: string
      - name: fields
        in: query
        description: |
          Comma-separated list of top-level fields to return. Unknown values
          are ignored silently. When `fields=` is supplied, the response is always narrowed; `id` and `name` are always present, and if no supplied keys match the whitelist the response collapses to just `{id, name}`.
        required: false
        schema:
          type: string
      - name: custom_data_points
        in: query
        description: |
          Optional. Controls embedding of a custom_data_points field on
          each person.

          - Omit the param entirely to leave custom_data_points out of the
            response (default).
          - Pass `*` to embed every account-declared person_data_points key
            (subject to the allow-list).
          - Pass a comma-separated list (e.g. `key1,key2`) to narrow the
            embed to that subset. Unknown keys are silently dropped.
          - Pass an empty value to embed an explicit empty
            custom_data_points object.
          - The wildcard always wins, so `*,foo` behaves identically to
            `*`.

          Select-type values are resolved to their human-readable option
          labels rather than the underlying stored value. Internal jsonb
          keys are never returned.
        required: false
        schema:
          type: string
      - name: first_name
        in: query
        description: First name includes this string (case-insensitive)
        schema:
          type: string
      - name: last_name
        in: query
        description: Last name includes this string (case-insensitive)
        schema:
          type: string
      - name: email
        in: query
        description: Email address includes this string (case-insensitive)
        schema:
          type: string
      - name: created_before
        in: query
        description: Only people created on or before this date
        schema:
          type: string
      - name: created_after
        in: query
        description: Only people created on or after this date
        schema:
          type: string
      - name: page
        in: query
        description: Page number (0-indexed)
        schema:
          default: 0
          type: integer
      - name: order_by
        in: query
        description: "Field to order by:\n * `first_name` \n * `last_name` \n * `email`
          \n * `created_at` \n "
        schema:
          default: created_at
          enum:
          - first_name
          - last_name
          - email
          - created_at
          type: string
      - name: order_direction
        in: query
        description: Order direction
        schema:
          default: asc
          enum:
          - asc
          - desc
          type: string
      responses:
        '200':
          description: no errors
          content:
            application/json:
              examples:
                success:
                  value:
                    data:
                    - id: 13
                      address:
                        city: ''
                        state: ''
                        street: ''
                        country: ''
                        zipcode: ''
                      email: buck.brown.27@vclab.fund
                      phone: "+11112223344"
                      created_at: '2024-08-06T18:32:52.335Z'
                      first_name: Buck
                      middle_name: ''
                      last_name: Brown
                      tag_list:
                      - tag1
                      - Another Tag
                      data:
                        custom_field: custom value
                        another field: another value
                      referred_by:
                        id: 54
                        email: florene.harvey.18@kirlinincllc.com
                        phone: "+918727727698"
                        first_name: Florene
                        middle_name:
                        last_name: Harvey
                      notes:
                      - body: This is a note
                        created_at: '2024-08-22T14:13:06.672Z'
                        author:
                          first_name: Apple
                          last_name: Owner
                      - body: This is another note
                        created_at: '2024-08-22T14:13:11.320Z'
                        author:
                          first_name: Apple
                          last_name: Owner
                      organizations_with_titles:
                      - id: 11
                        name: Green, Hand and Konopelski and Sons
                        title: CEO
                    pagination:
                      total_count: 1
                      current_page: 0
                      total_pages: 1
    patch:
      operationId: update_people
      x-agent-tool: true
      summary: Update People by ID
      tags:
      - Directory
      description: Updates up to 100 people by account-scoped ID and returns per-item
        updated and errors arrays. Requests with more than 100 items are rejected
        before any update runs. Remote picture URLs are fetched sequentially; omit
        pictures or split image-heavy updates into smaller batches.
      security:
      - api_key: []
      responses:
        '200':
          description: Batch update results, including partial success or an empty
            request.
        '400':
          description: Missing or malformed people parameter, or more than 100 items.
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                people:
                  type: array
                  maxItems: 100
                  items:
                    allOf:
                    - type: object
                      properties:
                        id:
                          type: string
                          pattern: "^\\d+$"
                      required:
                      - id
                    - "$ref": "#/components/schemas/person_update_attributes"
              required:
              - people
    post:
      operationId: create_people
      x-agent-tool: true
      summary: Create People
      tags:
      - Directory
      description: Adds People to your People Directory. Only the first 100 People
        per request will be processed.The response will have a created, duplicates,
        and errors array. The created array will contain the emails of the people
        that were created. The duplicates array will contain the emails of the people
        that were not created because they already exist in the database. The errors
        array will contain the emails of the people that were not created because
        of an error.
      security:
      - api_key: []
      parameters: []
      responses:
        '200':
          description: no errors or creations
          content:
            application/json:
              examples:
                no_creates:
                  value:
                    created: []
                    duplicates:
                    - test@test.com
                    errors: []
        '201':
          description: people created
          content:
            application/json:
              examples:
                at_least_one_created:
                  value:
                    created:
                    - test@test.com
                    duplicates: []
                    errors: []
        '400':
          description: bad request
        '401':
          description: Unauthorized
        '422':
          description: unprocessable entity
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                no_creates_and_has_errors:
                  value:
                    error:
                      code: validation_failed
                      message: Some people could not be created
                      details:
                      - 'test@test.com: Validation failed: Name can''t be blank'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                people:
                  type: array
                  items:
                    type: object
                    properties:
                      first_name:
                        type: string
                      middle_name:
                        type: string
                      last_name:
                        type: string
                      email:
                        type: string
                      phone:
                        type: string
                      linkedin:
                        type: string
                      tag_list:
                        type: string
                      custom_data_points:
                        type: object
                      note:
                        type: string
                      picture:
                        type: string
                        description: Profile photo, as a base64 data URI or a public
                          http(s) image URL. JPG, PNG, WebP, or GIF; max 5 MB.
                      address:
                        "$ref": "#/components/schemas/address"
                      referred_by:
                        "$ref": "#/components/schemas/associated_person"
                      organizations:
                        type: array
                        items:
                          type: object
                          properties:
                            name:
                              type: string
                            title:
                              type: string
                    required:
                    - first_name
                    - last_name
                    - email
              required:
              - people
  "/api/v1/person":
    post:
      operationId: upsert_person
      x-agent-tool: true
      summary: Create/Update a person
      tags:
      - Directory
      description: 'Create or Update an existing Person. The request can only contain
        one Person. A successful request will return a json object containing the
        success status, the person_id, and a list of changes in the form of field_name:
        [old_value, new_value].'
      security:
      - api_key: []
      parameters: []
      responses:
        '201':
          description: person updated
          content:
            application/json:
              examples:
                created:
                  value:
                    status: success
                    person_id: 1
                    changes:
                      first_name:
                      - Test
                      - Updated
                      last_name:
                      -
                      - Person
                      email:
                      -
                      - test@test.com
                      data:
                      - dog_name: Barkley
                      - dog_name: Charlie
        '400':
          description: bad request
        '401':
          description: Unauthorized
        '422':
          description: validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                person:
                  type: object
                  properties:
                    first_name:
                      type: string
                    middle_name:
                      type: string
                    last_name:
                      type: string
                    email:
                      type: string
                    phone:
                      type: string
                    linkedin:
                      type: string
                    tag_list:
                      type: string
                    remove_tag_list:
                      type: string
                      description: Comma-separated tags to remove. Tags not present
                        are ignored. A record is not created solely to remove tags.
                    custom_data_points:
                      type: object
                    note:
                      type: string
                    picture:
                      type: string
                      description: Profile photo, as a base64 data URI (data:image/png;base64,...)
                        or a public http(s) image URL. JPG, PNG, WebP, or GIF; max
                        5 MB.
                    address:
                      "$ref": "#/components/schemas/address"
                    referred_by:
                      "$ref": "#/components/schemas/associated_person"
                    organizations:
                      type: array
                      items:
                        type: object
                        properties:
                          name:
                            type: string
                          title:
                            type: string
                  required:
                  - first_name
                  - last_name
                  - email
              required:
              - person
  "/api/v1/people/{id}":
    get:
      operationId: get_person
      x-agent-tool: true
      summary: Show Person
      tags:
      - Directory
      description: 'Get a single Person by id. The default response shape is unchanged
        from prior versions (embeds notes, organizations_with_titles, referred_by).
        The optional include= and fields= parameters are purely opt-in - include=
        replaces the default include set with a caller-specified subset, and fields=
        narrows top-level columns. The response also carries a `picture` object (see
        schemas/attached_image) with a download `url`, or null when no photo is attached.

        '
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Person ID
        schema:
          type: integer
      - name: include
        in: query
        description: |
          Comma-separated list of nested resources to embed. Unknown values
          are ignored silently. Allowed values: notes, referred_by, organizations.
        required: false
        schema:
          type: string
      - name: fields
        in: query
        description: |
          Comma-separated list of top-level fields to return. Unknown values
          are ignored silently. When `fields=` is supplied, the response is always narrowed; `id` and `name` are always present, and if no supplied keys match the whitelist the response collapses to just `{id, name}`.
        required: false
        schema:
          type: string
      - name: custom_data_points
        in: query
        description: |
          Optional. Controls embedding of a custom_data_points field on
          the person.

          - Omit the param entirely to leave custom_data_points out of the
            response (default).
          - Pass `*` to embed every account-declared person_data_points key
            (subject to the allow-list).
          - Pass a comma-separated list (e.g. `key1,key2`) to narrow the
            embed to that subset. Unknown keys are silently dropped.
          - Pass an empty value to embed an explicit empty
            custom_data_points object.
          - The wildcard always wins, so `*,foo` behaves identically to
            `*`.

          Select-type values are resolved to their human-readable option
          labels rather than the underlying stored value. Internal jsonb
          keys are never returned.
        required: false
        schema:
          type: string
      responses:
        '200':
          description: person found
          content:
            application/json:
              examples:
                success:
                  value:
                    data:
                      id: 1
                      address:
                        city: ''
                        state: ''
                        street: ''
                        country: ''
                        zipcode: ''
                      email: showperson@example.com
                      phone: ''
                      created_at: '2024-08-06T18:32:52.335Z'
                      first_name: Show
                      middle_name: ''
                      last_name: Person
                      tag_list: []
                      data: {}
                      referred_by:
                      notes: []
                      organizations: []
        '404':
          description: not found
        '401':
          description: Unauthorized
    patch:
      operationId: update_person
      x-agent-tool: true
      summary: Update Person by ID
      tags:
      - Directory
      description: Updates the existing person selected by account-scoped ID; changing
        the email does not create a record.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Person ID
        schema:
          type: string
          pattern: "^\\d+$"
      responses:
        '200':
          description: Person updated
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Person not found
        '422':
          description: Validation failed
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                person:
                  "$ref": "#/components/schemas/person_update_attributes"
              required:
              - person
  "/api/v1/people/{id}/notes":
    post:
      operationId: add_person_note
      x-agent-tool: true
      summary: Add a note to a person
      tags:
      - Directory
      description: |
        Append a plain-text note to a person. The note is stored on the person and surfaced
        wherever person notes appear (including the prospect view if the person is a pipeline
        prospect's prospectable).
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Person ID
        schema:
          type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                note:
                  type: object
                  properties:
                    body:
                      type: string
                    context:
                      type: string
                  required:
                  - body
              required:
              - note
      responses:
        '201':
          description: note created
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/note_envelope"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Person not found
        '422':
          description: Validation error
  "/api/v1/research/investors":
    get:
      operationId: list_research_investors
      summary: List Decile Research Investors
      tags:
      - ResearchInvestors
      description: |
        Lists the shared "Decile Research" LP roster. The same roster is
        visible to every authenticated account. The response has a data array
        of roster entries and a pagination object with the total count, current
        page (0-indexed), and total pages. Each entry carries the research
        prospect `id` (use it with `POST /api/v1/research/investors/copy_to_pipeline`),
        the LP person details, organization name(s), stage name, and probability.
      security:
      - api_key: []
      parameters:
      - name: search
        in: query
        description: Case-insensitive filter over LP name, city, country, and organization
          name.
        required: false
        schema:
          type: string
      - name: stage
        in: query
        description: Filter by exact stage name.
        required: false
        schema:
          type: string
      - name: column
        in: query
        description: Sort column.
        required: false
        schema:
          type: string
          enum:
          - people.first_name
          - people.last_name
          - created_at
          - updated_at
      - name: direction
        in: query
        description: Sort direction (ascending by default).
        required: false
        schema:
          type: string
          enum:
          - asc
          - desc
      - name: page
        in: query
        description: Page number (0-indexed).
        required: false
        schema:
          type: integer
      responses:
        '200':
          description: Roster returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          description: Research prospect id; pass to copy_to_pipeline.
                        first_name:
                          type: string
                          nullable: true
                        middle_name:
                          type: string
                          nullable: true
                        last_name:
                          type: string
                          nullable: true
                        email:
                          type: string
                          nullable: true
                        linkedin:
                          type: string
                          nullable: true
                        city:
                          type: string
                          nullable: true
                        country:
                          type: string
                          nullable: true
                        organization_names:
                          type: array
                          items:
                            type: string
                        stage_name:
                          type: string
                          nullable: true
                        probability:
                          type: integer
                          nullable: true
                  pagination:
                    type: object
                    properties:
                      total_count:
                        type: integer
                      current_page:
                        type: integer
                      total_pages:
                        type: integer
        '400':
          description: invalid parameter (e.g. bad sort column or direction)
        '401':
          description: Unauthorized
  "/api/v1/research/investors/copy_to_pipeline":
    post:
      operationId: copy_research_investors_to_pipeline
      summary: Copy Decile Research Investors To Pipeline
      tags:
      - ResearchInvestors
      description: |
        Copies selected entries from the shared "Decile Research" LP roster
        into one of the caller's own pipeline stages. Supply the research
        prospect ids (from `GET /api/v1/research/investors`) and the target
        `stage_id` (a stage in your account's investor pipeline). Each copy is
        processed asynchronously; the response reports how many copies were
        enqueued and any ids that did not match the roster.
      security:
      - api_key: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - research_prospect_ids
              - stage_id
              properties:
                research_prospect_ids:
                  type: array
                  items:
                    type: string
                  description: Research prospect ids from the roster to copy.
                stage_id:
                  type: string
                  description: Target pipeline stage id in your account.
      responses:
        '202':
          description: Copies enqueued
          content:
            application/json:
              schema:
                type: object
                properties:
                  enqueued:
                    type: integer
                  stage_id:
                    type: string
                  invalid_ids:
                    type: array
                    items:
                      type: string
        '401':
          description: Unauthorized
        '403':
          description: Forbidden — caller may not add prospects to the target pipeline.
        '404':
          description: Stage not found
        '422':
          description: No valid research prospect ids supplied
  "/api/v1/prospects":
    post:
      operationId: create_prospect
      x-agent-tool: true
      summary: Create a single pipeline prospect (agent create_prospect tool)
      tags:
      - Prospects
      description: |
        Creates a Person or Organization prospect in a target pipeline — the
        moved-from-internal `create_prospect` agent tool (agent DATA tools live
        on /api/v1). PipelineProspect is polymorphic: `type: "Person"` for an
        individual contact, `type: "Organization"` for a company.

        IMPORTANT — `type` MUST match the target pipeline. A pipeline only
        accepts the prospectable type(s) named in its `allowed_prospect_types`
        (returned by `list_pipelines` and `get_pipeline`), a lowercase list such
        as `["organization"]`, `["person"]`, or `["organization","person"]`.
        create_prospect can only create people and companies, so read that list
        as: create `type: "Organization"` ONLY when it contains `organization`
        (deal / sourcing / company pipelines), and `type: "Person"` ONLY when it
        contains `person` (contacts / people pipelines). An organization-only
        pipeline REJECTS a Person; a person-only pipeline rejects an
        Organization. If the list contains NEITHER `organization` nor `person`
        (e.g. `capital_account`, `deal_memo`, `fund`, `user` — closing,
        deal-memo, capital-account pipelines), this pipeline holds neither
        people nor companies: do NOT call create_prospect on it — pick a
        people/company pipeline from list_pipelines. Sending a type the pipeline
        does not hold returns 422 `prospectable_type_not_allowed_on_pipeline`
        (the response body echoes the pipeline's `allowed_prospectables_type`).

        Person fields: first_name (required), last_name (required), email,
        email_source, title, organization_name, linkedin_url, notes. Pass
        organization_name to link the person to a company that ALREADY exists in
        the account - title rides onto that link, which is where the CRM shows a
        job title - and an unmatched name comes back as
        organization_link.status "organization_not_found" rather than creating a
        company. Only send an email a source states: never one built from a name
        and a company domain, never a company general inbox for a named person.
        Set email_source (user_provided | tool_verified | document) when you know
        where the address came from; a dropped address comes back as
        email_skipped with a reason, and the person is still created without it.
        Organization fields: company_name
        (required — mapped server-side to Organization.name), website,
        short_description, founder, region, lifecycle_stage, business_model,
        traction, legal_name, address, industries, notes. linkedin_url is not
        accepted for Organization. `notes` is append-only — each call adds a
        new activity-feed entry. Either type also accepts `stage_id`: pass it
        whenever the user names a stage, otherwise the prospect lands in the
        pipeline's first stage.

        Existing prospectables are reused, never duplicated: a Person with the
        same email (case-insensitive, per account) or an Organization with the
        same name is attached to the pipeline instead of re-created; re-adding
        a prospectable already in the pipeline is an idempotent no-op that
        returns the existing record.

        Agent-import routing params carry the Gmail/provider deal-import
        contract: `sender_email` (from-address of the source inbound email —
        enables known/cold triage routing for Organization deals),
        `triage: true` (provider imports with no inbound sender always land in
        the triage review pile), `deck_text` / `email_body` (extracted source
        text stored for triage assessment), and `source_refs` (Gmail
        thread/message/attachment refs used to persist the original deck
        server-side). Person prospects never re-route.

        Returns the created (or reused) record serialized as
        {id, type: "PipelineProspect", prospectable_type, fields}.
      security:
      - api_key: []
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - pipeline_id
              - type
              - fields
              properties:
                pipeline_id:
                  type: string
                  description: Target pipeline id (obscure id as returned by list_pipelines;
                    raw numeric ids also accepted).
                type:
                  type: string
                  description: 'Prospectable type to create. Must be creatable on
                    the target pipeline: use "Organization" only when the pipeline''s
                    allowed_prospect_types contains `organization` (deal/sourcing
                    pipelines), and "Person" only when it contains `person` (contacts
                    pipelines). If it contains neither (capital_account / deal_memo
                    / fund / user pipelines), do not use create_prospect — pick another
                    pipeline. Check via list_pipelines / get_pipeline first; a mismatch
                    returns 422 prospectable_type_not_allowed_on_pipeline.'
                  enum:
                  - Person
                  - Organization
                fields:
                  type: object
                  description: Per-type prospect fields (see the operation description
                    for the per-type allowlist).
                  properties:
                    first_name:
                      type: string
                    last_name:
                      type: string
                    email:
                      type: string
                    title:
                      type: string
                    linkedin_url:
                      type: string
                    company_name:
                      type: string
                    website:
                      type: string
                    short_description:
                      type: string
                    founder:
                      type: string
                    region:
                      type: string
                    lifecycle_stage:
                      type: string
                    business_model:
                      type: string
                    traction:
                      type: string
                    legal_name:
                      type: string
                    address:
                      type: object
                      properties:
                        street:
                          type: string
                        city:
                          type: string
                        state:
                          type: string
                        country:
                          type: string
                        zipcode:
                          type: string
                    industries:
                      type: array
                      items:
                        type: string
                    notes:
                      type: string
                      description: Append-only note text (max 2000 chars).
                    stage_id:
                      type: integer
                      description: 'Pipeline stage to create the prospect in. Set
                        it whenever the user names a stage ("add Acme in Diligence")
                        — omitting it silently lands the prospect in the pipeline''s
                        FIRST stage, which is rarely what they asked for. Must be
                        a stage of `pipeline_id`; a stage from another pipeline is
                        rejected rather than falling back. Ignored for triage imports
                        (`triage: true` or `sender_email` present on an Organization),
                        which are always forced into the review pile''s first stage.'
                sender_email:
                  type: string
                  description: From-address of the source inbound email. Enables known/cold
                    triage routing for Organization deals — pass it whenever the deal
                    came from an email.
                triage:
                  type: boolean
                  description: Set true for any imported deal so it lands in the triage
                    review pile (integration-provider imports with no inbound sender_email).
                deck_text:
                  type: string
                  description: Extracted pitch-deck text from the source email (max
                    50000 chars). Stored for triage assessment.
                email_body:
                  type: string
                  description: Plain-text body of the source email (max 20000 chars).
                    Stored for triage assessment.
                source_refs:
                  type: object
                  description: Gmail source references for the inbound pitch — the
                    server uses them to persist the original deck on the deal.
                  properties:
                    provider:
                      type: string
                      enum:
                      - gmail
                    thread_id:
                      type: string
                    message_id:
                      type: string
                    attachments:
                      type: array
                      items:
                        type: object
                        properties:
                          filename:
                            type: string
                          mime_type:
                            type: string
                idempotency_key:
                  type: string
                  description: Optional Redis SETNX dedupe key (24h TTL) — a retried
                    call with the same key replays the cached response instead of
                    re-executing.
      responses:
        '201':
          description: prospect created (or existing prospectable reused)
          content:
            application/json:
              examples:
                person:
                  value:
                    id: 4211
                    type: PipelineProspect
                    prospectable_type: Person
                    fields:
                      first_name: Jane
                      last_name: Doe
                      email: jane@example.com
                      stage_id: 7
                      notes: []
        '401':
          description: Unauthorized
        '403':
          description: Pipeline belongs to another account (ownership_denied)
        '404':
          description: Pipeline not found
        '422':
          description: invalid_type, field_not_allowed_for_type, prospectable_type_not_allowed_on_pipeline,
            or validation failure
  "/api/v1/prospects/bulk_create":
    post:
      operationId: bulk_create_prospects
      x-agent-tool: true
      summary: Bulk-create pipeline prospects with per-entry dedupe and outcomes
      tags:
      - Prospects
      description: |
        Creates MANY prospects in a target pipeline in ONE call (max 100
        entries) — use instead of looping create_prospect for lists of 3+
        prospects. Each entry mirrors create_prospect: `type` ("Person"
        default, or "Organization") plus `fields` (same per-type allowlist and
        validation as create_prospect; email is optional for Person).

        Entries are processed independently — partial success is normal and
        the batch never aborts because one entry fails. Each entry is deduped
        BEFORE creation (email match when present, else normalized first+last
        name; normalized company_name for Organizations), so re-sending a list
        after an interruption is safe and never creates duplicates.

        Dedupe reports WHERE the match lives, and the two cases mean different
        things:

        - already in the TARGET pipeline → `status: "skipped_duplicate"`,
          nothing written.
        - the person/company already exists ELSEWHERE in the account → the
          prospect IS created here, attached to the existing CRM record rather
          than a freshly minted duplicate. That row comes back as
          `status: "created"` with `reused_existing_record: true`,
          `matched_on`, and `existing_in_pipelines: [{id, name}]`. Report that
          an existing contact was reused and name those pipelines; do NOT
          describe it as a brand-new contact, and do NOT infer from it that the
          prospect was already in the target pipeline.

        Returns per-entry outcomes {index, status:
        created|skipped_duplicate|error, id, reason} plus a summary
        {total, created, reused_existing_record, skipped_duplicate, errors}.
        `reused_existing_record` is a SUBSET of `created`, not a separate
        bucket, so the buckets still sum as created + skipped_duplicate +
        errors = total.
      security:
      - api_key: []
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - pipeline_id
              - entries
              properties:
                pipeline_id:
                  type: string
                  description: Target pipeline id (obscure id as returned by list_pipelines;
                    raw numeric ids also accepted).
                entries:
                  type: array
                  minItems: 1
                  maxItems: 100
                  description: Prospect entries to load. Each is validated, deduped,
                    and created independently.
                  items:
                    type: object
                    required:
                    - fields
                    properties:
                      type:
                        type: string
                        enum:
                        - Person
                        - Organization
                        description: Defaults to Person — the common case for pasted
                          LP name lists.
                      fields:
                        type: object
                        description: Same per-type fields as create_prospect (Person
                          - first_name required, last_name required, email, email_source,
                          title, organization_name, linkedin_url, notes; Organization
                          - company_name required, website, etc.). Pass organization_name
                          to link a person to a company that ALREADY exists in the
                          account - title rides onto that link, which is where the
                          CRM shows a job title - and an unmatched name comes back
                          as organization_link.status "organization_not_found" rather
                          than creating a company. Only send an email a source states
                          - never one built from a name and a company domain, never
                          a company general inbox for a named person - and set email_source
                          when you know where it came from; a dropped address comes
                          back as email_skipped with a reason.
                idempotency_key:
                  type: string
                  description: Optional Redis SETNX dedupe key (24h TTL) — a retried
                    call with the same key replays the cached batch response instead
                    of re-executing. When omitted, an agent (run-JWT) caller gets
                    one DERIVED from the run plus the request body, so an identical
                    repeat inside one run writes nothing and its response carries
                    idempotent_replay true.
      responses:
        '200':
          description: per-entry outcomes + summary (partial success is a 200)
          content:
            application/json:
              examples:
                partial_success:
                  value:
                    results:
                    - index: 0
                      status: created
                      id: 4211
                      prospectable_type: Person
                    - index: 1
                      status: skipped_duplicate
                      id: 3180
                      reason: matched existing prospect in this pipeline on email
                    - index: 2
                      status: error
                      reason: 'Validation failed: Last name can''t be blank'
                    - index: 3
                      status: created
                      id: 4212
                      prospectable_type: Organization
                      reused_existing_record: true
                      matched_on: company_name
                      existing_in_pipelines:
                      - id: cD4wVu
                        name: Fund II Investors
                    summary:
                      total: 4
                      created: 2
                      reused_existing_record: 1
                      skipped_duplicate: 1
                      errors: 1
        '401':
          description: Unauthorized
        '403':
          description: Pipeline belongs to another account (ownership_denied)
        '404':
          description: Pipeline not found
        '422':
          description: entries_required or too_many_entries (max 100)
  "/api/v1/pipeline_prospects":
    get:
      operationId: list_pipeline_prospects
      x-agent-tool: true
      x-agent-category: context
      summary: List Pipeline Prospects
      tags:
      - PipelineProspects
      description: |
        Gets a paginated list of Prospects in a Pipeline. The response has a
        data array with the Prospects and a pagination object with the total
        count, current page, and total pages. Pages are 0-indexed, so the
        first page is page 0. The Prospects returned can be filtered and
        ordered. Order direction is ascending by default. Pass
        custom_data_points=* (or a narrowed subset) to embed each prospect's
        pipeline-declared custom data point values. For Select (Dropdown)
        data points, the human-readable option label is returned (not the
        underlying stored value). Every prospect row carries a `url` — a
        direct Hub deep-link that opens the prospect in its pipeline; include
        it when reporting a prospect so the user can navigate straight to it.
      security:
      - api_key: []
      parameters:
      - name: pipeline_id
        in: query
        description: Pipeline ID
        required: true
        schema:
          type: string
      - name: include
        in: query
        description: |
          Comma-separated list of nested resources to embed. Allowed:
          `notes`, `referred_by`, `assigned`, `stage`, `capital_account`.
          Unknown values are ignored. Default (no `include=` supplied) embeds
          `assigned`, `stage`, `referred_by`, `notes`. Supplying `include=`
          REPLACES the default set.

          `capital_account` embeds fund/KYC/onboarding details on
          closing-pipeline prospects (`prospectable_type` ==
          `FirmAdmin::CapitalAccount`); the key is omitted for other
          prospect types.
        required: false
        schema:
          type: string
      - name: fields
        in: query
        description: |
          Comma-separated top-level fields to return. Allowed: `id`,
          `last_contact`, `next_contact`, `rating`, `probability`,
          `prospectable_type`, `prospectable_id`, `capital_commitment`,
          `created_at`, `updated_at`. Unknown fields are ignored. `id` and
          `name` are always present.
        required: false
        schema:
          type: string
      - name: custom_data_points
        in: query
        description: |
          Optional. Controls embedding of a custom_data_points field on
          each prospect.

          - Omit the param entirely to leave custom_data_points out of the
            response (default).
          - Pass `*` to embed every pipeline-declared custom data point
            key (subject to the pipeline allow-list).
          - Pass a comma-separated list (e.g. `key1,key2`) to narrow the
            embed to that subset. Unknown keys are silently dropped.
          - Pass an empty value to embed an explicit empty
            custom_data_points object.
          - The wildcard always wins, so `*,foo` behaves identically to
            `*`.

          Select-type values are resolved to their human-readable option
          labels rather than the underlying stored value. Internal jsonb
          keys (e.g. actionable_url) are never returned.
        required: false
        schema:
          type: string
      - name: name
        in: query
        description: Name includes this string (case-insensitive)
        schema:
          type: string
      - name: created_before
        in: query
        description: Only prospects created on or before this date
        schema:
          type: string
      - name: created_after
        in: query
        description: Only prospects created on or after this date
        schema:
          type: string
      - name: updated_before
        in: query
        description: Only prospects updated on or before this date
        schema:
          type: string
      - name: updated_after
        in: query
        description: Only prospects updated on or after this date
        schema:
          type: string
      - name: last_contact_before
        in: query
        description: Only prospects last contacted on or before this date
        schema:
          type: string
      - name: last_contact_after
        in: query
        description: Only prospects last contacted on or after this date
        schema:
          type: string
      - name: next_contact_before
        in: query
        description: Only prospects with a next contact on or before this date
        schema:
          type: string
      - name: next_contact_after
        in: query
        description: Only prospects with a next contact on or after this date
        schema:
          type: string
      - name: stage_name
        in: query
        description: The name of the Pipeline Stage that the Prospect is in includes
          this string (case-insensitive)
        schema:
          type: string
      - name: stage_id
        in: query
        description: The ID of the Pipeline Stage that the Prospect is in
        schema:
          type: string
      - name: stage_before
        in: query
        description: The Prospect is in this stage ID or any stage before it in the
          pipeline
        schema:
          type: string
      - name: stage_after
        in: query
        description: The Prospect is in this stage ID or any stage after it in the
          pipeline
        schema:
          type: string
      - name: tags
        in: query
        description: The prospect has these tags (comma-separated)
        schema:
          type: string
      - name: page
        in: query
        description: Page number (0-indexed)
        schema:
          default: 0
          type: integer
      - name: order_by
        in: query
        description: "Field to order by:\n * `name` \n * `last_contact` \n * `next_contact`
          \n * `created_at` \n * `updated_at` \n * `stage` \n "
        schema:
          default: stage
          enum:
          - name
          - last_contact
          - next_contact
          - created_at
          - updated_at
          - stage
          type: string
      - name: order_direction
        in: query
        description: Order direction
        schema:
          default: asc
          enum:
          - asc
          - desc
          type: string
      responses:
        '200':
          description: no errors
          content:
            application/json:
              examples:
                success:
                  value:
                    data:
                    - id: 15
                      created_at: '2024-08-06T18:33:18.863Z'
                      updated_at: '2024-08-06T18:33:18.863Z'
                      rating:
                      probability: 25
                      prospectable_type: Person
                      last_contact: '2024-08-20T13:01:00.000Z'
                      next_contact: '2024-09-02T12:00:00.000Z'
                      data:
                        custom_field: custom value
                        another field: another value
                      tag_list:
                      - tag1
                      - tags2
                      stage:
                        id: 246
                        name: Added
                      referred_by:
                        id: 3
                        email: user@apple.com
                        phone:
                        first_name: Alice
                        middle_name: ''
                        last_name: Albright
                      notes:
                      - body: This is a note
                        created_at: '2024-08-28T18:26:41.063Z'
                        author:
                          first_name: Apple
                          last_name: Owner
                      assigned_name: Young Buck
                      url: https://myfirm.decilehub.com/people/xY12ab/edit_prospect
                    pagination:
                      total_count: 1
                      current_page: 0
                      total_pages: 1
        '400':
          description: bad request
        '404':
          description: not found
    post:
      operationId: create_pipeline_prospects
      x-agent-tool: true
      summary: Create Pipeline Prospects
      tags:
      - PipelineProspects
      description: Creates Person/Organization pipeline prospects for a given pipeline.
        Only the first 100 organizations and first 100 people will be processed.A
        valid request will return a json object with two keys, organizations and people.
        Each key will have a created, duplicates, and errors array. The created array
        will contain the names of the prospects that were created. The duplicates
        array will contain the names of the prospects that were not created because
        they already exist in the database. The errors array will contain the names
        of the prospects that were not created because of an error.
      security:
      - api_key: []
      parameters: []
      responses:
        '200':
          description: no errors or creations
          content:
            application/json:
              examples:
                no_creates:
                  value:
                    organizations:
                      created: []
                      duplicates:
                      - Existing Org
                      errors: []
                    people:
                      created: []
                      duplicates:
                      - Test Person
                      errors: []
        '201':
          description: pipeline prospect created
          content:
            application/json:
              examples:
                at_least_one_created:
                  value:
                    organizations:
                      created:
                      - Test API Org
                      duplicates: []
                      errors: []
                    people:
                      created: []
                      duplicates:
                      - Test Person
                      errors: []
        '400':
          description: bad request
        '401':
          description: Unauthorized
        '404':
          description: not found
        '422':
          description: unprocessable entity
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                no_creates_and_has_errors:
                  value:
                    error:
                      code: validation_failed
                      message: Some prospects could not be created
                      details:
                        organizations:
                          created: []
                          duplicates: []
                          errors:
                          - ": Validation failed: Name can't be blank"
                        people:
                          created: []
                          duplicates: []
                          errors: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                pipeline_id:
                  type: string
                stage_id:
                  type: string
                prospects:
                  type: object
                  properties:
                    organizations:
                      type: array
                      items:
                        type: object
                        properties:
                          name:
                            type: string
                          probability:
                            type: number
                          rating:
                            type: number
                          tag_list:
                            type: string
                          note:
                            type: string
                          company_url:
                            type: string
                          short_description:
                            type: string
                          custom_data_points:
                            type: object
                          address:
                            "$ref": "#/components/schemas/address"
                          referred_by:
                            "$ref": "#/components/schemas/associated_person"
                          primary_contact:
                            "$ref": "#/components/schemas/associated_person"
                        required:
                        - name
                    people:
                      type: array
                      items:
                        type: object
                        properties:
                          first_name:
                            type: string
                          middle_name:
                            type: string
                          last_name:
                            type: string
                          email:
                            type: string
                          phone:
                            type: string
                          linkedin:
                            type: string
                          probability:
                            type: number
                          rating:
                            type: number
                          tag_list:
                            type: string
                          note:
                            type: string
                          address:
                            "$ref": "#/components/schemas/address"
                          referred_by:
                            "$ref": "#/components/schemas/associated_person"
                          organizations:
                            type: array
                            items:
                              type: object
                              properties:
                                name:
                                  type: string
                                title:
                                  type: string
                        required:
                        - first_name
                        - last_name
                        - email
              required:
              - pipeline_id
              - prospects
    patch:
      operationId: update_pipeline_prospects
      x-agent-tool: true
      summary: Update pipeline prospects
      tags:
      - PipelineProspects
      description: |
        Updates one or more prospects in a pipeline. Send `pipeline_id` (query or body) and a JSON array `prospects`.
        Each element can identify the prospect by `id` (preferred), exact `name`, or for Person prospectables by `email`.

        **Stage moves:** pass `stage_id` at the top level and/or per prospect. Omit `stage_id` to leave the stage unchanged.

        **Metadata:** `probability`, `rating`, `next_contact`, and `last_contact` can be updated. Send JSON `null` for
        `next_contact` or `last_contact` to clear them

        **Notes:** pass `note` to append a note (same as the single-prospect upsert).

        **Tags:** pass `tag_list` (comma-separated) to add tags and/or `remove_tag_list` (comma-separated) to remove tags. Tags not present on a prospect are ignored on removal.

        **Single prospect:** `PATCH /api/v1/pipeline_prospects/{id}` accepts the same fields without wrapping in `prospects`.
      security:
      - api_key: []
      parameters:
      - name: pipeline_id
        in: query
        required: true
        description: Pipeline ID (can also be sent in the JSON body)
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - pipeline_id
              - prospects
              properties:
                pipeline_id:
                  type: string
                stage_id:
                  type: string
                  description: Applied to each prospect item that does not set its
                    own `stage_id`
                prospects:
                  type: array
                  items:
                    type: object
                    properties:
                      id:
                        type: string
                        description: Pipeline prospect id
                      name:
                        type: string
                        description: Exact display name match (ambiguous if multiple)
                      email:
                        type: string
                        description: Person prospect email (must match exactly one
                          prospect in the pipeline)
                      stage_id:
                        type: string
                      probability:
                        type: integer
                      rating:
                        type: integer
                      next_contact:
                        type: string
                        format: date-time
                        nullable: true
                      last_contact:
                        type: string
                        format: date-time
                        nullable: true
                      note:
                        type: string
                      tag_list:
                        type: string
                        description: Comma-separated tags to add to the prospect.
                      remove_tag_list:
                        type: string
                        description: Comma-separated tags to remove from the prospect.
                          Tags not present are ignored.
                      custom_data_points:
                        type: object
                      assigned_id:
                        type: integer
                        nullable: true
                        description: AccountUser id to assign as the prospect owner.
                          Pass null to unassign. Must belong to the prospect's account.
      responses:
        '200':
          description: |
            At least one prospect updated. The `errors` array may be non-empty when some rows failed to
            resolve or update while others succeeded. Each error entry contains an `error` message and,
            when available, the originating `params` or the resolved prospect's `id`/`name`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  updated_prospects:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        name:
                          type: string
                  errors:
                    type: array
                    description: Per-row errors. Possible messages include `No prospect
                      found with id <id>`, `No prospect found with email <email>`,
                      `Multiple prospects match email <email>`, `Invalid id <id>`,
                      and `No unambiguous matching prospect found`.
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        name:
                          type: string
                        error:
                          type: string
                        params:
                          type: object
        '400':
          description: bad request
        '404':
          description: |
            Returned when the pipeline itself is not found, when `prospects` is empty, or when every row
            failed to resolve to a matching prospect (so there were no successful updates).
        '401':
          description: Unauthorized
  "/api/v1/pipeline_prospects/{id}":
    patch:
      operationId: update_pipeline_prospect
      x-agent-tool: true
      summary: Update one pipeline prospect
      tags:
      - PipelineProspects
      description: |
        Same fields as the batch `PATCH /api/v1/pipeline_prospects` body (except `prospects`). `pipeline_id` is required
        (query or body). The prospect id is taken from the path. Unlike the batch endpoint, this endpoint returns a
        singular response shape (no `prospects`/`errors` wrapper).
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: Pipeline prospect id.
        required: true
        schema:
          type: string
      - name: pipeline_id
        in: query
        description: Id of the pipeline the prospect belongs to.
        required: true
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                pipeline_id:
                  type: string
                stage_id:
                  type: string
                probability:
                  type: integer
                rating:
                  type: integer
                next_contact:
                  type: string
                  format: date-time
                  nullable: true
                last_contact:
                  type: string
                  format: date-time
                  nullable: true
                note:
                  type: string
                tag_list:
                  type: string
                  description: Comma-separated tags to add to the prospect.
                remove_tag_list:
                  type: string
                  description: Comma-separated tags to remove from the prospect. Tags
                    not present are ignored.
                custom_data_points:
                  type: object
                assigned_id:
                  type: integer
                  nullable: true
                  description: AccountUser id to assign as the prospect owner. Pass
                    null to unassign. Must belong to the prospect's account.
      responses:
        '200':
          description: Prospect updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  id:
                    type: integer
                  name:
                    type: string
        '400':
          description: |
            Bad request. Returned when the id in the path is malformed (e.g. not an integer and not a valid
            encoded id) or when required parameters are missing.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
        '404':
          description: 'Pipeline not found, or no prospect with the given id exists
            in this pipeline.

            '
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
        '422':
          description: 'The prospect was resolved but the update failed (for example
            invalid datetime value).

            '
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
        '401':
          description: Unauthorized
    delete:
      operationId: delete_pipeline_prospect
      x-agent-tool: true
      summary: Remove one prospect from its pipeline
      tags:
      - PipelineProspects
      description: |
        Removes a prospect from its pipeline by id. This is a RECOVERABLE soft
        discard, not a permanent deletion: the prospect disappears from the
        pipeline board, but the underlying person or company record, its notes,
        and its history are all retained in the CRM, and re-adding the same
        record to the pipeline restores the prospect.

        Use this when the user asks to remove, exclude, drop, or take a
        prospect off a pipeline. Describe it to the user as removing them from
        the pipeline, never as permanently deleting or erasing their data.

        Do NOT use it to record an outcome the pipeline already models — a
        prospect who passed, declined, or went cold belongs in the matching
        stage (move them with `update_pipeline_prospect`), not off the board.

        The prospect id is taken from the path; no `pipeline_id` is needed. Two
        refusals are deliberate and must be relayed rather than worked around:
        prospects in a closing / start-fund-closing / SPV-closing pipeline
        cannot be removed through the API (they drive downstream closing
        state), and the caller's user must hold pipeline edit access.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: Pipeline prospect id.
        required: true
        schema:
          type: string
      responses:
        '200':
          description: |
            Prospect removed from the pipeline (soft discard); returns
            { success, message }.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
        '401':
          description: Unauthorized
        '403':
          description: |
            The prospect belongs to a closing pipeline (removal is refused there),
            or the API key's user lacks pipeline edit access.
        '404':
          description: |
            No kept prospect with the given id exists in the caller's account
            (already-removed prospects also return 404).
        '422':
          description: The prospect was resolved but the discard failed validation.
  "/api/v1/pipeline_prospects/{pipeline_prospect_id}/notes":
    post:
      operationId: add_pipeline_prospect_note
      x-agent-tool: true
      summary: Add a note to a pipeline prospect
      tags:
      - PipelineProspects
      description: |
        Append a plain-text note in the context of a pipeline prospect. To match the in-app
        behavior, the note is stored against the prospect's prospectable (Person /
        Organization) when one exists, falling back to the prospect itself otherwise. When
        `note.context` is omitted, the prospect's pipeline name is used as the context.
      security:
      - api_key: []
      parameters:
      - name: pipeline_prospect_id
        in: path
        required: true
        description: Pipeline prospect ID (numeric or obscure-encoded)
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                note:
                  type: object
                  properties:
                    body:
                      type: string
                    context:
                      type: string
                      description: Optional. Defaults to the prospect's pipeline name.
                  required:
                  - body
              required:
              - note
      responses:
        '201':
          description: note created
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/note_envelope"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Pipeline prospect not found
        '422':
          description: Validation error
  "/api/v1/pipeline_prospect":
    post:
      operationId: upsert_pipeline_prospect
      x-agent-tool: true
      summary: Create/Update a Pipeline Prospect
      tags:
      - PipelineProspects
      description: |
        Create or update an existing pipeline prospect in a given pipeline. The request must contain exactly one of
        `person` or `organization`. On **create**, `stage_id` selects the stage. On **update**, the prospect's stage is
        **not** changed unless you pass `apply_stage_id_to_existing: true` (then `stage_id` is applied to an existing
        prospect in this pipeline only).

        To move stages or update `next_contact`, `probability`, `rating`, or append notes without using this upsert,
        prefer `PATCH /api/v1/pipeline_prospects` (batch) or `PATCH /api/v1/pipeline_prospects/{id}` (single).

        A successful response includes `success`, `pipeline_prospect_id`, and `changes` (field name to [old, new]).
      security:
      - api_key: []
      parameters: []
      responses:
        '201':
          description: pipeline prospect created/updated
          content:
            application/json:
              examples:
                created:
                  value:
                    status: success
                    pipeline_prospect_id: 1
                    changes:
                      first_name:
                      -
                      - Jane
                      last_name:
                      -
                      - Jones
                      email:
                      -
                      - jjones@decilehub.com
        '400':
          description: bad request
        '401':
          description: Unauthorized
        '404':
          description: not found
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                pipeline_id:
                  type: string
                stage_id:
                  type: string
                apply_stage_id_to_existing:
                  type: boolean
                  description: If true and the person/organization already has a prospect
                    in this pipeline, apply `stage_id` (or first stage) to move them.
                prospect:
                  type: object
                  properties:
                    probability:
                      type: number
                    rating:
                      type: number
                    tag_list:
                      type: string
                    custom_data_points:
                      type: object
                    organization:
                      type: object
                      properties:
                        name:
                          type: string
                        note:
                          type: string
                        company_url:
                          type: string
                        short_description:
                          type: string
                        custom_data_points:
                          type: object
                        address:
                          "$ref": "#/components/schemas/address"
                        referred_by:
                          "$ref": "#/components/schemas/associated_person"
                        primary_contact:
                          "$ref": "#/components/schemas/associated_person"
                      required:
                      - name
                    person:
                      type: object
                      properties:
                        first_name:
                          type: string
                        middle_name:
                          type: string
                        last_name:
                          type: string
                        email:
                          type: string
                        phone:
                          type: string
                        linkedin:
                          type: string
                        tag_list:
                          type: string
                        note:
                          type: string
                        custom_data_points:
                          type: object
                        address:
                          "$ref": "#/components/schemas/address"
                        referred_by:
                          "$ref": "#/components/schemas/associated_person"
                        organizations:
                          type: array
                          items:
                            type: object
                            properties:
                              name:
                                type: string
                              title:
                                type: string
                      required:
                      - first_name
                      - last_name
                      - email
              required:
              - pipeline_id
              - prospect
  "/api/v1/pipeline_prospects/{pipeline_prospect_id}/action_executions":
    get:
      operationId: list_pipeline_action_executions
      x-agent-tool: true
      summary: List a prospect's pipeline action executions
      tags:
      - PipelineProspects
      description: |
        Lists the `PipelineActionExecution` rows for a single prospect — i.e. the
        actions queued to run (or that have run / been skipped) on this prospect
        at its current stage. Each row includes a human-readable description and
        a per-type `target` block with foreign-key names resolved server-side, so
        a caller (especially an agent / MCP client) can show the user *what the
        action would do* before triggering it.

        **Defaults to `state=pending`.** Pass `state=completed`, `state=skipped`,
        or `state=all` to broaden. The state filter applies to ROOT executions —
        children of returned roots are always included regardless of state, so
        callers can see what's already been done within an in-progress task.

        **`final_action_for_stage`** rows (the workflow gate that holds a
        prospect at its current stage — not user-pickable) are excluded by
        default. Pass `include_final=true` to include them.

        ### Response shape

        Tree-shaped — `data` contains only ROOT executions (each represents
        one task in the UI), and each root carries a `children` array with its
        subtasks. Subtasks within a root are intended to execute in order.
        Roots are ordered by `position`, then `scheduled_at` (NULLs last);
        children are ordered by `position` within their parent.

        - `prospect`: `{ id, name, stage_id, stage_name }` envelope so the caller
          knows the prospect's current stage in the same round-trip.
        - `data`: array of root executions. Each row:
          - `id` (string) — execution id (integer rendered as string).
          - `state` — one of `pending`, `completed`, `skipped`.
          - `action_type` — one of the 15 `PipelineAction` types
            (`send_email`, `move_to_stage`, `assign_user`, `apply_tag`,
            `set_variable`, `record_task_completed`, `chat`, `execute_job`,
            `copy_to_pipeline`, `final_action_for_stage`,
            `move_to_stage_from_date_cell`, `base_post_reply`,
            `start_questionnaire`, `create_user_notification`).
          - `description` — human-readable summary, e.g. `"Send email Welcome"`,
            `"Change stage to Qualified"`, `"Apply tag Lead"`. Plaintext (no
            `<em>` tags).
          - `scheduled_at` — ISO8601 or `null`.
          - `automate` — boolean; whether the periodic worker will fire this
            without manual completion.
          - `action_id` — id of the source `PipelineAction` template (may be
            `null` if the template was deleted; `type` is denormalised on the
            execution and survives template deletion).
          - `parent_id` — `null` on root rows, set to the parent root's id on
            children (children appear inside their root's `children` array, but
            still carry `parent_id` so flat traversals see the link).
          - `target` — type-specific resolved payload (see below). May be
            `null` for types with no resolvable target.
          - `next_pending_id` (root rows only) — id of the next leaf the
            caller should pass to the execute endpoint (first pending child
            in position order, else the root if it's still pending, else
            `null`). Mirrors the UI's "next" leaf. Prefer this over the
            root's own id when the root has pending children: executing a
            root with pending children CASCADES — the worker runs every
            child in order then the root, in one call, with no per-subtask
            note prompt. Walking via `next_pending_id` mirrors the UI's
            per-subtask flow.
          - `children` (root rows only) — array of child rows with the same
            shape (minus their own `children` and `next_pending_id`).

        ### Per-type `target` shape

        | `action_type` | `target` keys |
        | --- | --- |
        | `send_email` | `email_template_id`, `email_template_name` |
        | `move_to_stage` | `stage_id`, `stage_name` |
        | `final_action_for_stage` | `stage_id`, `stage_name` (omitted/blank stage_id means "hold at this stage") |
        | `move_to_stage_from_date_cell` | `stage_id`, `stage_name` |
        | `assign_user` | `assignee_id`, `assignee_name` |
        | `apply_tag` | `tag` |
        | `set_variable` | `variable_name`, `value`, `use_today` |
        | `execute_job` | `job_class` (raw class name; review before triggering) |
        | `copy_to_pipeline` | `pipeline_id`, `pipeline_name` |
        | `chat` | `chat_agenda_id`, `chat_agenda_name` |
        | `record_task_completed` | `task_description` |
        | `base_post_reply` | `reply_text` (truncated to 200 chars) |

        Other `action_type` values currently return `null` for `target`.
      security:
      - api_key: []
      parameters:
      - name: pipeline_prospect_id
        in: path
        description: Pipeline prospect id.
        required: true
        schema:
          type: string
      - name: state
        in: query
        description: |
          Filter ROOT executions by state. Defaults to `pending`. Pass `all`
          to return roots in every state. Children of returned roots are
          always included regardless of state.
        required: false
        schema:
          type: string
          enum:
          - pending
          - completed
          - skipped
          - all
          default: pending
      - name: include_final
        in: query
        description: 'When true, include `final_action_for_stage` rows. Default false.

          '
        required: false
        schema:
          type: boolean
          default: false
      responses:
        '200':
          description: ok
          content:
            application/json:
              examples:
                pending_actions:
                  summary: Default pending root executions for a prospect, including
                    a task with subtasks
                  value:
                    prospect:
                      id: 183
                      name: Acme Fund I — LP 17
                      stage_id: 394
                      stage_name: Qualified
                    data:
                    - id: '421'
                      state: pending
                      action_type: send_email
                      description: Send email Welcome
                      scheduled_at: '2026-05-08T09:00:00Z'
                      automate: true
                      action_id: '1812'
                      parent_id:
                      target:
                        email_template_id: '55'
                        email_template_name: Welcome
                      next_pending_id: '421'
                      children: []
                    - id: '422'
                      state: pending
                      action_type: record_task_completed
                      description: Submit BD review
                      scheduled_at:
                      automate: false
                      action_id: '1813'
                      parent_id:
                      target:
                        task_description: Submit BD review
                      next_pending_id: '424'
                      children:
                      - id: '423'
                        state: completed
                        action_type: record_task_completed
                        description: Collect BD docs
                        scheduled_at:
                        automate: false
                        action_id: '1814'
                        parent_id: '422'
                        target:
                          task_description: Collect BD docs
                      - id: '424'
                        state: pending
                        action_type: record_task_completed
                        description: Send to compliance
                        scheduled_at:
                        automate: false
                        action_id: '1815'
                        parent_id: '422'
                        target:
                          task_description: Send to compliance
        '400':
          description: invalid `state` value
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: unauthorized (missing/invalid API key)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: prospect not found in the API key's account
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/pipeline_prospects/{pipeline_prospect_id}/action_executions/{id}/execute":
    post:
      operationId: execute_pipeline_action_execution
      x-agent-tool: true
      summary: Execute a single pipeline action execution
      tags:
      - PipelineProspects
      description: |
        Executes one `PipelineActionExecution` for the prospect — sends the
        templated email, moves the stage, applies the tag, runs the configured
        background job, etc., according to the execution's `action_type` and
        snapshotted `data`, then advances any chained automated actions.

        **The action runs as authored.** This endpoint only triggers an
        existing pending execution. An optional `note` body field is honored
        for `record_task_completed` actions (attached when marking the task
        complete) — see the request body below. Other
        body fields are silently dropped.

        **One execution per call.** This endpoint deliberately does not loop or
        bulk-execute — agent / MCP callers are expected to list pending
        executions, surface each action's resolved description to the user,
        and call this endpoint per confirmed action.

        **Idempotency:** if the execution is already `completed` or `skipped`,
        returns 422 with `code: already_finalized`. Validation gates or
        missing prerequisites surface as 422 with
        `code: action_execution_error`.

        Response shape mirrors `GET /action_executions`: a `prospect`
        envelope (so callers see the resulting `stage_id` / `stage_name` if
        the action transitioned the prospect) plus a single `execution`
        object with the post-execute `state`, resolved `description`, and
        per-type `target` block (see the GET endpoint for the per-type
        target table).
      security:
      - api_key: []
      parameters:
      - name: pipeline_prospect_id
        in: path
        required: true
        description: Pipeline prospect id.
        schema:
          type: string
      - name: id
        in: path
        required: true
        description: PipelineActionExecution id.
        schema:
          type: string
      requestBody:
        required: false
        description: |
          Optional `note` and `data` mirror the UI's task-complete modal.

          `note` is only meaningful for `record_task_completed` actions; it's
          saved as a note on the prospect's associated Person/Organization
          (or on the prospect itself when it has no such associated record),
          with the pipeline name as `context` and the API key's user as
          `author`. Ignored for any other action type.

          `data` is allowlisted per action type — anything outside the
          allowlist is silently dropped. Callers cannot mutate
          `state` / `scheduled_at` from this endpoint, and `data` keys for
          action types not listed below are ignored.

          | action_type    | allowed `data` keys                   | notes |
          | -------------- | ------------------------------------- | ----- |
          | `apply_tag`    | `tag`                                 | description rewritten to `"Apply tag <tag>"` |
          | `assign_user`  | `assign_to_id`                        | `account_va` / `account_fa` stored as `{ assign_to_role: ... }` |
          | `set_variable` | `variable_name`, `value`, `use_today` | when `use_today` is truthy, stored `value` is `Date.current.to_s`; description rewritten |
          | `send_email`   | `rich_subject`, `body`, `cc`, `bcc`, `attachments_attributes`, `new_attachment_signed_ids`, `email_from_actor`, `scheduled_at`, `email_template_id`, `custom_substituted_variables`, `test_email`, `test_email_to_address`, `notify_users` | All keys are submitted **flat** at `data.<key>` — the controller nests the content-shaped keys (`rich_subject`, `body`, `cc`, `bcc`, `attachments_attributes`, `new_attachment_signed_ids`, `email_from_actor`, `scheduled_at`) under `pipeline_email_content` server-side before persisting. **Callers MUST NOT pre-nest** under `pipeline_email_content` — doing so silently drops the overrides. `cc` / `bcc` accept the same heterogeneous form as `POST /api/v1/emails` (raw email string, `{person_id: "..."}`, or `{internal_user_id: <id>}`); they're normalized to a comma-joined string before persisting. `test_email_to_address` is singular here (mirrors the UI); `POST /api/v1/emails` uses the plural `test_email_to_addresses` array. |
        content:
          application/json:
            schema:
              type: object
              properties:
                note:
                  type: string
                  description: |
                    Free-text note to record on the prospect alongside the
                    task completion. Only honored for `record_task_completed`
                    action types.
                data:
                  type: object
                  description: |
                    Per-type confirm-time overrides. Allowlisted — see the
                    table above. Keys outside the per-type allowlist are
                    silently dropped.
                  additionalProperties: true
              examples:
                record_task_completed_with_note:
                  value:
                    note: Called them, left a voicemail.
                apply_tag_with_data:
                  value:
                    data:
                      tag: High Priority
                set_variable_with_today:
                  value:
                    data:
                      variable_name: Person.last_contacted_at
                      use_today: '1'
                send_email_overrides:
                  summary: Override subject/body and add a cc recipient at execute
                    time
                  value:
                    data:
                      rich_subject: Welcome to Acme Fund I
                      body: "<p>Hi {{Person.first_name}}, thanks for joining.</p>"
                      cc:
                      - ops@example.com
                      - person_id: '1042'
                      custom_substituted_variables:
                        custom.greeting: Hello
      responses:
        '200':
          description: ok
          content:
            application/json:
              examples:
                executed:
                  summary: Executed a send_email action; prospect stage unchanged
                  value:
                    prospect:
                      id: 183
                      name: Acme Fund I — LP 17
                      stage_id: 394
                      stage_name: Qualified
                    execution:
                      id: '421'
                      state: completed
                      action_type: send_email
                      description: Send email Welcome
                      scheduled_at: '2026-05-08T09:00:00Z'
                      automate: true
                      action_id: '1812'
                      target:
                        email_template_id: '55'
                        email_template_name: Welcome
        '401':
          description: unauthorized (missing/invalid API key)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: forbidden
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: prospect or execution not found in the API key's account
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: |
            Execution already completed/skipped (`code: already_finalized`) or
            worker rejected the action (`code: action_execution_error`).
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                already_finalized:
                  value:
                    error:
                      code: already_finalized
                      message: Execution is already completed and cannot be re-executed.
                action_execution_error:
                  value:
                    error:
                      code: action_execution_error
                      message: KYC must be passed before this action can run.
                invalid_data:
                  value:
                    error:
                      code: invalid_data
                      message: Tag can't be blank
  "/api/v1/pipeline_prospects/{pipeline_prospect_id}/action_executions/{id}/preview":
    post:
      operationId: preview_pipeline_action_execution
      x-agent-tool: true
      summary: Preview a pending send_email action execution
      tags:
      - PipelineProspects
      description: |
        Renders the would-be sent email for a pending `send_email`
        `PipelineActionExecution` — substitutes variables, resolves cc/bcc,
        and reports unresolved variables / blocker warnings — without
        sending or mutating the execution. The recipient is fixed to the
        prospect that owns the execution; you can override the email body,
        subject, cc/bcc, attachments, scheduled_at, the from-actor, the
        template, and `custom_substituted_variables` at preview time.

        **Defaults come from the execution's persisted
        `data[:pipeline_email_content]`** (the authored content snapshot).
        Any override key you submit replaces the corresponding persisted
        value for this preview only — nothing is written back to the
        execution. Override keys are flat at the top level of the request
        body (mirrors `POST /api/v1/emails/preview`), not nested under a
        wrapper.

        **`cc` / `bcc` semantics:** absent → fall back to the execution's
        persisted cc/bcc. Explicit `[]` clears (distinct from absent).
        Accepted entry shapes mirror `POST /api/v1/emails/preview`: raw
        email string, `{person_id: "..."}`, or
        `{internal_user_id: <numeric_id>}`. Malformed entries return 422
        `invalid_recipient_format`.

        **Non-send_email actions return 422 `unsupported_action_type`.**
        Use `GET /api/v1/pipeline_prospects/{id}/action_executions` to
        confirm the `action_type` first.

        Response mirrors `POST /api/v1/emails/preview` plus a
        `pipeline_action_execution` block carrying the execution `id`,
        `state`, and resolved `description`.
      security:
      - api_key: []
      parameters:
      - name: pipeline_prospect_id
        in: path
        required: true
        description: Pipeline prospect id.
        schema:
          type: string
      - name: id
        in: path
        required: true
        description: PipelineActionExecution id.
        schema:
          type: string
      requestBody:
        required: false
        description: |
          All keys optional. Omitted keys fall back to the execution's
          persisted `pipeline_email_content`. Unknown keys are silently
          dropped per allowlist semantics.
        content:
          application/json:
            schema:
              type: object
              properties:
                rich_subject:
                  type: string
                  description: HTML subject override.
                body:
                  type: string
                  description: HTML body override.
                cc:
                  type: array
                  description: |
                    Override cc list. Each entry is one of: raw email string,
                    `{person_id}`, or `{internal_user_id}`. Absent → fall back
                    to persisted cc. Explicit `[]` clears.
                  items:
                    oneOf:
                    - type: string
                    - type: object
                      properties:
                        person_id:
                          type: string
                    - type: object
                      properties:
                        internal_user_id:
                          type: integer
                bcc:
                  type: array
                  description: Same shape as `cc`.
                  items:
                    oneOf:
                    - type: string
                    - type: object
                      properties:
                        person_id:
                          type: string
                    - type: object
                      properties:
                        internal_user_id:
                          type: integer
                attachments_attributes:
                  type: object
                  description: Override attachment attribute map (mirrors the UI form).
                  additionalProperties: true
                new_attachment_signed_ids:
                  type: array
                  description: ActiveStorage signed blob ids for newly uploaded attachments.
                  items:
                    type: string
                email_from_actor:
                  type: string
                  description: From-actor URI override (e.g. `user:42`).
                scheduled_at:
                  type: string
                  format: date-time
                  description: ISO 8601 schedule time override.
                email_template_id:
                  type: string
                  description: Override the template used for rendering this preview.
                custom_substituted_variables:
                  type: object
                  description: Map of `custom.<name>` → value substitutions.
                  additionalProperties:
                    type: string
              examples:
                no_overrides:
                  summary: Preview with persisted content (no overrides)
                  value: {}
                with_overrides:
                  summary: Override subject + add a cc person
                  value:
                    rich_subject: Welcome — updated
                    cc:
                    - person_id: '1042'
                    custom_substituted_variables:
                      custom.greeting: Hello
      responses:
        '200':
          description: Preview envelope.
          content:
            application/json:
              examples:
                ok:
                  summary: Preview rendered, ready_to_send=true
                  value:
                    recipient:
                      person_id: '5512'
                      name: Jane Investor
                      email: jane@example.com
                      missing_email: false
                    from:
                      actor_uri: user:42
                      display: Houston Searcy <houston@decilegroup.com>
                    cc:
                    - ops@example.com
                    bcc: []
                    rendered:
                      subject: Welcome to Acme Fund I
                      body_html: "<p>Hi Jane, thanks for joining.</p>"
                      body_text: Hi Jane, thanks for joining.
                    template:
                      id: '320'
                      name: Welcome
                      pipeline_id: 43nOmK6a
                      decile_admin_only: false
                    variables:
                      resolved:
                      - name: Person.first_name
                        value: Jane
                      unresolved: []
                    attachments:
                    - id: '88'
                      filename: welcome.pdf
                      source: template
                    warnings: []
                    ready_to_send: true
                    pipeline_action_execution:
                      id: '421'
                      state: pending
                      description: Send email Welcome
        '401':
          description: unauthorized (missing/invalid API key)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: prospect or execution not found in the API key's account
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: |
            Action is not `send_email` (`code: unsupported_action_type`), or
            a recipient list entry was malformed (`code:
            invalid_recipient_format`).
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                unsupported_action_type:
                  value:
                    error:
                      code: unsupported_action_type
                      message: Preview is only supported for send_email actions; this
                        execution is apply_tag.
                invalid_recipient_format:
                  value:
                    error:
                      code: invalid_recipient_format
                      message: Recipient entry must be a string email, {person_id},
                        or {internal_user_id}.
  "/api/v1/pipeline_prospects/{pipeline_prospect_id}/attachments":
    get:
      operationId: list_prospect_attachments
      x-agent-tool: true
      summary: List a prospect's files (direct attachments + deal-intake questionnaire
        uploads)
      tags:
      - PipelineProspects
      description: |
        Lists the files available for an investment-pipeline prospect, merged
        from two sources:

        - `source: "direct"` — CRM attachments on the prospect's underlying
          organization/person (uploaded in the Hub UI or via
          `POST /api/v1/files` with `attachable_type`/`attachable_id`). These
          carry a `name`.
        - `source: "questionnaire"` — files the founder uploaded through a
          deal-intake questionnaire (e.g. Submit Your Company): pitch decks,
          cap tables, supporting docs. These carry the questionnaire
          `item_id` the file was uploaded for, which matches the filename
          custom field stored on the organization record.

        Each entry has `signed_id`, `filename`, `content_type`, `byte_size`,
        `source`, `name`, `item_id`, and `uploaded_at`. Pass an entry's
        `signed_id` to the download endpoint to fetch the bytes.

        **Scope guardrails:** only prospects in `investment`-type pipelines
        are served — requests for prospects in closing/investor/other
        pipelines return 403. Questionnaire uploads are listed only for
        deal-intake questionnaire types (`submit_your_company`); files
        respondents upload through LP onboarding, tax forms, or other
        LP-facing questionnaires are never exposed through this endpoint.
      security:
      - api_key: []
      parameters:
      - name: pipeline_prospect_id
        in: path
        description: Pipeline prospect id.
        required: true
        schema:
          type: string
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                          nullable: true
                          description: Attachment record id (direct source only) —
                            pass to the DELETE endpoint. Null on questionnaire entries,
                            which cannot be deleted through this API.
                        signed_id:
                          type: string
                          description: ActiveStorage signed blob id — pass to the
                            download endpoint.
                        filename:
                          type: string
                        content_type:
                          type: string
                        byte_size:
                          type: integer
                        source:
                          type: string
                          enum:
                          - direct
                          - questionnaire
                        name:
                          type: string
                          nullable: true
                          description: Attachment display name (direct source only).
                        item_id:
                          type: string
                          nullable: true
                          description: Questionnaire item id the file was uploaded
                            for (questionnaire source only).
                        uploaded_at:
                          type: string
                          format: date-time
                          nullable: true
              examples:
                mixed_sources:
                  value:
                    data:
                    - signed_id: eyJfcmFpbHMi...
                      filename: Pitch Deck_Acme_April2026.pdf
                      content_type: application/pdf
                      byte_size: 1118698
                      source: questionnaire
                      name:
                      item_id: questionnaire_item_eb9a7707
                      uploaded_at: '2026-06-12T14:54:11Z'
                    - signed_id: eyJfcmFpbHMi...
                      filename: term_sheet.pdf
                      content_type: application/pdf
                      byte_size: 84211
                      source: direct
                      name: Term Sheet
                      item_id:
                      uploaded_at: '2026-06-14T09:12:44Z'
        '401':
          description: unauthorized
        '403':
          description: prospect is not in an investment pipeline, or the API key's
            user lacks pipeline access
        '404':
          description: prospect not found in the caller's account
    post:
      operationId: upload_prospect_attachment
      x-agent-tool: true
      summary: Attach a file directly to a prospect
      tags:
      - PipelineProspects
      description: |
        Uploads a file as a direct attachment on the prospect's underlying
        organization/person — the same attachment list the prospect page
        shows. Unlike `POST /api/v1/files` with `attachable_type`/
        `attachable_id`, this does NOT create a data room copy; use it when
        the file belongs on the prospect only.

        Expects `multipart/form-data` with `attachment[file]=<upload>` and
        optional `attachment[name]` (defaults to the filename without its
        extension, matching the web UI). No file-type allow-list is applied
        (web-UI parity; the data room upload endpoint is stricter).

        Same scope guardrail as the list endpoint: investment-type pipelines
        only (403 otherwise). Requires pipeline edit access
        (`edit_prospects?`), not just read.
      security:
      - api_key: []
      parameters:
      - name: pipeline_prospect_id
        in: path
        description: Pipeline prospect id.
        required: true
        schema:
          type: string
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                attachment:
                  type: object
                  properties:
                    file:
                      type: string
                      format: binary
                    name:
                      type: string
                      description: Optional display name (defaults to filename without
                        extension).
                  required:
                  - file
      responses:
        '201':
          description: attachment created; returns the same entry shape as the list
            endpoint (with `id` and `signed_id`)
        '400':
          description: missing attachment[file]
        '401':
          description: unauthorized
        '403':
          description: prospect is not in an investment pipeline, or the API key's
            user lacks pipeline edit access
        '404':
          description: prospect not found in the caller's account
        '422':
          description: attachment failed validation
  "/api/v1/pipeline_prospects/{pipeline_prospect_id}/attachments/{attachment_id}":
    delete:
      operationId: delete_prospect_attachment
      x-agent-tool: true
      summary: Delete a direct prospect attachment
      tags:
      - PipelineProspects
      description: |
        Deletes one direct attachment (`source: "direct"`) by its `id` from
        the list endpoint. Hard delete, mirroring the web UI.

        Questionnaire uploads are NOT deletable through this API — they
        belong to the founder's submitted questionnaire response (deleting
        one requires re-syncing the response's answers, a flow the web UI
        owns) and carry no `id` in the list payload. Note the addressing
        asymmetry is deliberate: download is blob-level (`signed_id`),
        delete is row-level (`attachment_id`), because a blob can be shared
        by multiple records while the Attachment row is the deletable unit.

        Ids outside this prospect's direct attachment set return 404. Same
        guardrails as the other verbs: investment-type pipelines only,
        `edit_prospects?` access required.
      security:
      - api_key: []
      parameters:
      - name: pipeline_prospect_id
        in: path
        description: Pipeline prospect id.
        required: true
        schema:
          type: string
      - name: attachment_id
        in: path
        description: Attachment id from a `source:"direct"` entry of the list endpoint.
        required: true
        schema:
          type: integer
      responses:
        '200':
          description: deleted; returns { success, message }
        '401':
          description: unauthorized
        '403':
          description: prospect is not in an investment pipeline, or the API key's
            user lacks pipeline edit access
        '404':
          description: prospect not found, or attachment id not in this prospect's
            direct attachment set
  "/api/v1/pipeline_prospects/{pipeline_prospect_id}/attachments/{signed_id}/download":
    get:
      operationId: download_prospect_attachment
      x-agent-tool: true
      summary: Download a prospect attachment by signed blob id
      tags:
      - PipelineProspects
      description: |
        Downloads one file listed by the prospect attachments endpoint. The
        `signed_id` must belong to the given prospect's attachment set (the
        same merged, gated list the index endpoint serves) — signed ids from
        other prospects, other accounts, or non-intake questionnaires return
        404. Responses over 50 MB redirect (302) to a short-lived signed
        storage URL instead of streaming inline.
      security:
      - api_key: []
      parameters:
      - name: pipeline_prospect_id
        in: path
        description: Pipeline prospect id.
        required: true
        schema:
          type: string
      - name: signed_id
        in: path
        description: ActiveStorage signed blob id from the attachments list endpoint.
        required: true
        schema:
          type: string
      responses:
        '200':
          description: file content with attachment Content-Disposition
        '302':
          description: redirect to a short-lived signed storage URL (files over 50
            MB)
        '401':
          description: unauthorized
        '403':
          description: prospect is not in an investment pipeline, or the API key's
            user lacks pipeline access
        '404':
          description: prospect not found, signed id invalid, or blob not in this
            prospect's attachment set
  "/api/v1/pipelines":
    get:
      operationId: list_pipelines
      x-agent-tool: true
      x-agent-category: context
      summary: List pipelines for the current account
      tags:
      - Pipelines
      description: |
        Returns the active pipelines for the current account. Pass
        `kind=investor` to filter to investor pipelines. Rows are ordered
        with the account's primary investor pipeline first (when one is
        marked); prefer the `is_primary: true` row when multiple investor
        pipelines exist. For entity-scoped operations, match `entity_id`
        exactly rather than inferring from the name or `is_primary`. The `id` field is the value to pass to any
        pipeline-scoped endpoint (e.g. `/api/v1/pipeline_prospects?pipeline_id=…`).
      security:
      - api_key: []
      parameters:
      - name: kind
        in: query
        description: Optional pipeline-type filter. Unknown values return 422.
        required: false
        schema:
          type: string
          enum:
          - recruiting
          - investor
          - investment
          - connector
          - closing
          - clean
          - custom
          - capital_call
          - company_funding_round
          - account_organization_fund
          - portfolio
          - event_attendance
          - base_post
          - newsletter
          - start_fund_closing
          - investor_research
          - lp_consent
          - spv_closing
      responses:
        '200':
          description: Pipelines found
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    account_id:
                      type: string
                    entity_id:
                      type: integer
                      nullable: true
                      description: Associated account-organization/entity id; null
                        for account-wide pipelines.
                    id:
                      type: string
                    name:
                      type: string
                    type:
                      type: string
                      description: Pipeline category (matches the `kind` query values).
                    is_primary:
                      type: boolean
                      description: TRUE on the account's primary pipeline of this
                        type.
                    allowed_prospect_types:
                      type: array
                      description: 'Prospectable types this pipeline can hold (lowercase,
                        e.g. ["organization"], ["person"], ["organization", "person"],
                        or non-prospect tokens like ["capital_account"] / ["deal_memo"]
                        / ["fund", "organization"]). For create_prospect: pass type="Organization"
                        only if this list contains `organization`, type="Person" only
                        if it contains `person`. If it contains neither, this pipeline
                        does not hold people or companies — do not create prospects
                        on it.'
                      items:
                        type: string
                  required:
                  - account_id
                  - entity_id
                  - id
                  - name
                  - type
                  - is_primary
                  - allowed_prospect_types
        '401':
          description: Unauthorized
        '422':
          description: Unknown kind value
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
  "/api/v1/pipelines/{id}":
    get:
      operationId: get_pipeline
      x-agent-tool: true
      summary: Retrieves info for a specific pipeline
      tags:
      - Pipelines
      description: Returns basic information for a specific pipeline and its stages.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        description: Pipeline id.
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Pipeline found
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  total_prospect_count:
                    type: integer
                  allowed_prospect_types:
                    type: array
                    description: 'Prospectable types this pipeline can hold (lowercase,
                      e.g. ["organization"], ["person"], ["organization","person"],
                      or non-prospect tokens like ["capital_account"] / ["deal_memo"]
                      / ["fund","organization"]). For create_prospect: pass type="Organization"
                      only if this list contains `organization`, type="Person" only
                      if it contains `person`. If it contains neither, this pipeline
                      does not hold people or companies — do not create prospects
                      on it.'
                    items:
                      type: string
                  stages:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        prospect_count:
                          type: integer
        '401':
          description: Unauthorized
  "/api/v1/pipelines/{id}/metrics":
    get:
      summary: Pipeline metrics — current state + 7-day-ago snapshot
      operationId: get_pipeline_metrics
      x-agent-tool: true
      tags:
      - Pipelines
      description: |
        Returns aggregate counts/values for a pipeline plus a 7-days-ago
        snapshot for week-over-week deltas. The caller is expected to
        compute deltas in its prose layer.
      security:
      - api_key: []
      parameters:
      - in: path
        name: id
        description: Pipeline id.
        required: true
        schema:
          type: string
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  current_state:
                    "$ref": "#/components/schemas/PipelineMetricsSnapshot"
                  snapshot_7d_ago:
                    "$ref": "#/components/schemas/PipelineMetricsSnapshot"
        '401':
          description: Unauthorized
        '404':
          description: pipeline not found or out of account scope
  "/api/v1/pipelines/{pipeline_id}/data_points":
    post:
      operationId: create_pipeline_data_point
      x-agent-tool: true
      summary: Create a custom data point for a pipeline type (account-wide fan-out)
      tags:
      - Pipelines
      description: 'Create a custom data point (Variable) on the account, scoped to
        the **type** of the targeted pipeline (investor / company / etc). Data points
        are not attached to a single pipeline — they live at the account level under
        a type-specific context (e.g. `investor_data_points`, `company_data_points`).
        The `pipeline_id` in the path is used only to derive that type context. When
        `add_as_column: true`, the resulting kanban column is fanned out to **every
        pipeline of that type** in the account, not just the targeted pipeline (this
        matches the UI behavior). Account admin authorization is required (account
        admins, or decile admins acting on the account). The `select` format requires
        a non-empty `select_options` array.'
      security:
      - api_key: []
      parameters:
      - name: pipeline_id
        in: path
        required: true
        description: Obscure ID of any pipeline of the desired type. Used to derive
          the data point's type context (investor / company / etc). The data point
          applies to the account and, when `add_as_column` is true, fans out as a
          column to all pipelines of this type in the account — not just this one.
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - data_point
              properties:
                data_point:
                  type: object
                  required:
                  - name
                  - format
                  properties:
                    name:
                      type: string
                      description: Display name for the data point
                    format:
                      type: string
                      description: Format key — controls how the data point is rendered
                        and validated.
                      enum:
                      - string
                      - paragraph
                      - url
                      - number
                      - date
                      - date_us
                      - currency_us
                      - currency_eu
                      - percent
                      - percent_rounded
                      - select
                      - file_url
                    description:
                      type: string
                      description: Optional helper text shown to users editing the
                        data point.
                    add_as_column:
                      type: boolean
                      description: If true, surface the data point as a kanban column.
                        The column is added to **all pipelines of the same type**
                        as the targeted `pipeline_id` in the account (fan-out behavior
                        matching the UI), not just the targeted pipeline.
                    select_options:
                      type: array
                      items:
                        type: string
                      description: Required when `format` is `select`. The dropdown
                        options.
      responses:
        '201':
          description: Data point created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                  name:
                    type: string
                  value:
                    type: string
                  format:
                    type: string
                    description: The short format key (echoes the request).
                  context:
                    type: string
                    description: Variable context, e.g. `investor_data_points`.
                  description:
                    type: string
                    nullable: true
                  add_as_column:
                    type: boolean
                  select_options:
                    type: array
                    nullable: true
                    items:
                      type: string
        '400':
          description: Request body missing the `data_point` wrapper
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: Forbidden (caller is not an account admin or decile admin for
            the account)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Pipeline not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: Validation failed (missing/invalid name, unknown format, or
            select format with empty `select_options`)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/deals/share":
    post:
      operationId: create_deal_share
      x-agent-tool: true
      summary: Create or update a Deal Share
      tags:
      - Deal Shares
      description: 'Share a deal from the caller''s account into the network feed.
        Idempotent per organization: if a Deal Share already exists for the given
        organization in the caller''s account its attributes are updated; otherwise
        a new record is created. The target organization must belong to the caller''s
        account — use POST /api/v1/organization to create or upsert one first. Pass
        selected_files to attach existing org / deal-memo / term-sheet files the caller
        account owns; foreign or unknown ids are skipped. The response attachments
        list confirms what was attached.'
      security:
      - api_key: []
      parameters: []
      responses:
        '201':
          description: Deal Share created
          content:
            application/json:
              examples:
                created:
                  value:
                    success: true
                    deals_share_id: 42
                    organization_id: 7
                    created_at: '2026-04-20T12:00:00Z'
                    updated_at: '2026-04-20T12:00:00Z'
                    attachments:
                    - id: 99
                      name: pitch_deck.pdf
        '200':
          description: Existing Deal Share updated
          content:
            application/json:
              examples:
                updated:
                  value:
                    success: true
                    deals_share_id: 42
                    organization_id: 7
                    created_at: '2026-04-20T12:00:00Z'
                    updated_at: '2026-04-20T12:30:00Z'
                    attachments:
                    - id: 99
                      name: pitch_deck.pdf
        '400':
          description: Request body missing deals_share wrapper
        '401':
          description: Unauthorized
        '404':
          description: Organization not found (not in caller's account)
        '422':
          description: Validation failed (missing required field or invalid value)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                deals_share:
                  type: object
                  properties:
                    organization_id:
                      type: string
                      description: ID of an Organization in the caller's account
                    company_name:
                      type: string
                    the_bet:
                      type: string
                      description: Thesis for why this deal is interesting
                    referring_manager_name:
                      type: string
                    referring_manager_email:
                      type: string
                      format: email
                    founder:
                      type: string
                    short_description:
                      type: string
                    region:
                      type: string
                    country_address:
                      type: string
                    company_email:
                      type: string
                      format: email
                    deck_url:
                      type: string
                    round:
                      type: string
                      description: Funding round key.
                      enum:
                      - accelerator
                      - angel
                      - pre_seed
                      - seed
                      - a
                      - b
                      - c
                      - d
                      - e
                      - f
                      - mezzanine
                    anticipated_close_date:
                      type: string
                      format: date
                    why:
                      type: string
                    investor_role:
                      type: string
                      enum:
                      - investing_lead
                      - investing_not_lead
                      - invested_prior_round
                      - maybe_investing
                      - advising
                      - not_investing
                      - interesting_deal
                      - founder
                    deal_source:
                      type: string
                      enum:
                      - cold_inreach
                      - conference
                      - direct_founder_relationship
                      - personal_network
                      - pitch_session
                      - prospecting
                      - vc_deal_flow
                      - portfolio_company
                      - known_founder_referral
                      - known_investor_invested
                      - known_investor_not_invested
                      - syndication_group
                      - venture_studio
                    traction_metrics:
                      type: string
                    amount_raising:
                      type: number
                    deal_terms:
                      type: string
                    valuation:
                      type: number
                    industry_list:
                      type: array
                      items:
                        type: string
                    selected_files:
                      type: array
                      items:
                        type: string
                      description: Ids of existing files the caller account owns to
                        attach (e.g. organization_123, deal_memo_45, term_sheet_67).
                        Foreign/unknown ids are skipped.
                  required:
                  - organization_id
                  - company_name
                  - the_bet
                  - referring_manager_name
                  - referring_manager_email
              required:
              - deals_share
  "/api/v1/deals/shares":
    get:
      operationId: list_deal_shares
      x-agent-tool: true
      summary: List the deal shares submitted by the current account
      tags:
      - Deal Shares
      description: |
        Returns the deal shares the caller's account has submitted into the network feed.
        Each row carries the full submission attributes plus the computed `stage`.

        Filter with `search`, `industry`, `stage`, and `round`. Sort with `column` +
        `direction`. Pagination is keyset — pass `pagination.next_page_token` from a
        response back as `page_token=` to fetch the next page.
      security:
      - api_key: []
      parameters:
      - name: search
        in: query
        required: false
        description: Free-text match over `company_name`, `short_description`, `region`,
          and `country_address`.
        schema:
          type: string
      - name: industry
        in: query
        required: false
        description: Filter to shares tagged with this industry.
        schema:
          type: string
      - name: stage
        in: query
        required: false
        description: Computed stage bucket filter.
        schema:
          type: string
          enum:
          - New Deals (Filtered on Thesis)
          - Unfiltered New Deals
          - Expired Deals
          - Expiring Deals
      - name: round
        in: query
        required: false
        description: Funding round filter.
        schema:
          type: string
          enum:
          - accelerator
          - angel
          - pre_seed
          - seed
          - a
          - b
          - c
          - d
          - e
          - f
          - mezzanine
      - name: column
        in: query
        required: false
        description: Sort column. Unknown columns fall back to `company_name`.
        schema:
          type: string
          default: company_name
          enum:
          - company_name
          - created_at
          - amount_raising
          - anticipated_close_date
          - round
      - name: direction
        in: query
        required: false
        description: Sort direction. Sending anything other than `asc`/`desc` returns
          400.
        schema:
          type: string
          default: asc
          enum:
          - asc
          - desc
      - name: page_token
        in: query
        required: false
        description: Opaque keyset cursor returned by a previous call as `pagination.next_page_token`.
        schema:
          type: string
      - name: per_page
        in: query
        required: false
        description: Page size. Capped at 100; defaults to 25.
        schema:
          type: integer
          default: 25
          maximum: 100
      responses:
        '200':
          description: paginated list of the caller account's deal shares
          content:
            application/json:
              examples:
                default:
                  value:
                    data:
                    - id: 42
                      organization_id: 7
                      company_name: Acme Corp
                      the_bet: Strong team and differentiated wedge
                      referring_manager_name: Alice Referrer
                      referring_manager_email: alice@example.com
                      round: seed
                      amount_raising: 2000000
                      industry_list:
                      - Fintech
                      stage: Unfiltered New Deals
                      created_at: '2026-04-20T12:00:00Z'
                      updated_at: '2026-04-20T12:00:00Z'
                    pagination:
                      next_page_token: eyJjb21wYW55X25hbWUiOiJBY21lIENvcnAiLCJpZCI6NDJ9
                      has_more: true
              schema:
                type: object
                required:
                - data
                - pagination
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/DealShareDetail"
                  pagination:
                    type: object
                    properties:
                      next_page_token:
                        type: string
                        nullable: true
                      has_more:
                        type: boolean
        '400':
          description: invalid parameter
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
  "/api/v1/deals/shares/{id}":
    get:
      operationId: get_deal_share
      x-agent-tool: true
      summary: Get a single deal share owned by the current account
      tags:
      - Deal Shares
      description: |
        Returns one deal share submitted by the caller's account, with the full
        submission attributes and the computed `stage`. Returns 404 if the share
        does not belong to the caller's account.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Deal share id.
        schema:
          type: integer
      responses:
        '200':
          description: The deal share
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/DealShareDetail"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
    delete:
      operationId: delete_deal_share
      x-agent-tool: true
      summary: Delete a deal share owned by the current account
      tags:
      - Deal Shares
      description: |
        Permanently deletes a deal share owned by the caller's account. The lookup
        is scoped to the caller's account, so an unknown id or one belonging to
        another account returns 404 and deletes nothing. This action cannot be undone.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Deal share id.
        schema:
          type: integer
      responses:
        '200':
          description: The deal share was deleted
          content:
            application/json:
              examples:
                deleted:
                  value:
                    success: true
                    message: Deal share deleted successfully
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/deals/shares/{id}/copy_to_pipeline":
    post:
      operationId: copy_deal_to_pipeline
      x-agent-tool: true
      summary: Copy a shared deal into a pipeline stage
      tags:
      - Deal Shares
      description: Copy a network-feed shared deal into one of the caller account's
        pipeline stages. Creates (or reuses) an organization in the caller's account
        and adds it as a prospect to the given stage, asynchronously. The pipeline_stage_id
        MUST belong to the caller's account. Requires the caller account's shared
        deal feed to be unlocked (deal_sharing_unlocked_until present and in the future);
        decile admins and full-access account admins bypass that gate.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Deals::Share id from the network feed
        schema:
          type: string
      responses:
        '202':
          description: Copy accepted and enqueued
          content:
            application/json:
              examples:
                accepted:
                  value:
                    success: true
                    deals_share_id: 42
                    pipeline_stage_id: 7
                    status: processing
        '401':
          description: Unauthorized
        '403':
          description: Shared deal feed locked for the caller account
        '404':
          description: Deal share not found in the network feed, or pipeline stage
            not in the caller's account
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                pipeline_stage_id:
                  type: string
                  description: Target PipelineStage id in the caller's account
              required:
              - pipeline_stage_id
  "/api/v1/deals/shares/ai_auto_fill":
    post:
      operationId: enrich_deal_share
      x-agent-tool: true
      summary: AI auto-fill enrichment for a deal share
      tags:
      - Deal Shares
      description: Run AI auto-fill enrichment for a deal share. Given an organization
        in the caller's account and a list of deal-share form field names, the model
        reasons over the data Hub already holds (organization profile, deal memos,
        notes, documents) and returns suggested values for those fields. The organization
        MUST belong to the caller's account. The call runs the LLM synchronously,
        so response latency tracks the model call. Field names use the form-input
        shape, e.g. `deals_share[region]` or `deals_share[industry_list][]`.
      security:
      - api_key: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                organization_id:
                  type: string
                  description: ID of an Organization in the caller's account
                fields:
                  type: array
                  items:
                    type: string
                  description: Deal-share form field names to enrich, e.g. ['deals_share[region]',
                    'deals_share[industry_list][]']
              required:
              - organization_id
              - fields
      responses:
        '200':
          description: Enrichment completed
          content:
            application/json:
              examples:
                completed:
                  value:
                    data:
                      region: US
                      country_address: United States
                      traction_metrics: "$100M ARR, 50% YoY growth"
                    status: completed
        '401':
          description: Unauthorized
        '404':
          description: Organization not found in the caller's account
        '422':
          description: Enrichment failed (e.g. the AI response could not be parsed)
          content:
            application/json:
              examples:
                failed:
                  value:
                    status: failed
                    error: Invalid JSON response from AI service
  "/api/v1/deal_memos":
    get:
      operationId: list_deal_memos
      x-agent-tool: true
      summary: List deal memos for the current account
      tags:
      - Deal Memos
      description: |
        Returns a paginated list of deal memos belonging to the caller's
        account. Each row is a compact summary suitable for triage; use
        `GET /api/v1/deal_memos/{id}` for the full payload (counterfactual,
        ratings, links, question answers).

        Pagination is keyset — pass `pagination.next_page_token` from a
        response back as `page_token=` to fetch the next page.
      security:
      - api_key: []
      parameters:
      - name: organization_id
        in: query
        required: false
        description: Filter to deal memos for a specific organization id.
        schema:
          type: integer
      - name: stage
        in: query
        required: false
        description: Filter to a single stage (active, review, ic_review, ic_review_submitted,
          ic_pre_approved, ic_approved, closed).
        schema:
          type: string
          enum:
          - active
          - review
          - ic_review
          - ic_review_submitted
          - ic_pre_approved
          - ic_approved
          - closed
      - name: page_token
        in: query
        required: false
        description: Opaque keyset cursor returned by a previous call as `pagination.next_page_token`.
        schema:
          type: string
      - name: per_page
        in: query
        required: false
        description: Page size. Capped at 100; defaults to 25.
        schema:
          type: integer
          default: 25
          maximum: 100
      responses:
        '200':
          description: paginated list of deal memos
          content:
            application/json:
              examples:
                default:
                  value:
                    data:
                    - id: 101
                      stage: active
                      investment_type: initial
                      organization_id: 7
                      organization_name: Acme Inc.
                      close_type:
                      created_at: '2026-05-14T12:00:00Z'
                      updated_at: '2026-05-14T12:00:00Z'
                      account_review_enabled: true
                    pagination:
                      next_page_token: eyJpZCI6MTAxfQ
                      has_more: true
              schema:
                type: object
                required:
                - data
                - pagination
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        stage:
                          type: string
                        investment_type:
                          type: string
                        organization_id:
                          type: integer
                        organization_name:
                          type: string
                          nullable: true
                        close_type:
                          type: string
                          nullable: true
                        created_at:
                          type: string
                          format: date-time
                        updated_at:
                          type: string
                          format: date-time
                        account_review_enabled:
                          type: boolean
                          description: Whether the caller's account has the deal memo
                            review workflow enabled (account-level flag). When false,
                            submit_for_review and close are unavailable from the web
                            UI.
                  pagination:
                    type: object
                    properties:
                      next_page_token:
                        type: string
                        nullable: true
                      has_more:
                        type: boolean
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '422':
          description: Invalid stage filter
    post:
      operationId: create_deal_memo
      x-agent-tool: true
      summary: Start a Deal Memo
      tags:
      - Deal Memos
      description: Start a new deal memo for an organization in the caller's account.
        The config template is resolved automatically from the account's latest deal
        memo config. Fails with 422 if the organization already has a non-closed deal
        memo. The target organization must belong to the caller's account. Set `copy_last_deal_memo`
        to true to clone the organization's most recent closed deal memo instead of
        starting a blank one. File attachments are not supported via the API.
      security:
      - api_key: []
      parameters: []
      responses:
        '201':
          description: Deal Memo created
          content:
            application/json:
              examples:
                created:
                  value:
                    data:
                      id: 101
                      stage: active
                      investment_type: initial
                      created_at: '2026-05-14T12:00:00Z'
                      organization_id: 7
                      organization_name: Acme Inc.
        '400':
          description: Request body missing deal_memo wrapper or organization_id
        '401':
          description: Unauthorized
        '403':
          description: Caller cannot manage investments for this account
        '404':
          description: Organization not found (not in caller's account)
        '422':
          description: Validation failed, or the organization already has a non-closed
            deal memo
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                deal_memo:
                  type: object
                  properties:
                    organization_id:
                      type: string
                      description: ID of an Organization in the caller's account
                    investment_type:
                      type: string
                      enum:
                      - initial
                      - follow on
                      - secondary
                      description: Investment type. Defaults to initial.
                    description:
                      type: string
                    counterfactual:
                      type: string
                      description: Counterfactual analysis text for the deal memo
                    company_url:
                      type: string
                      description: Company URL; updates the organization's company_url
                        if set
                    start_ai_orchestrator:
                      type: boolean
                      description: Whether to start the AI orchestrator for this deal
                        memo
                    copy_last_deal_memo:
                      type: boolean
                      description: Copy the organization's last closed deal memo instead
                        of starting a blank one
                    links_attributes:
                      type: array
                      description: Reference links to attach to the deal memo
                      items:
                        type: object
                        properties:
                          name:
                            type: string
                            description: Link label
                          link:
                            type: string
                            description: Link URL
                    organization_attributes:
                      type: object
                      description: Nested organization updates applied alongside the
                        deal memo
                      properties:
                        id:
                          type: string
                          description: ID of the organization
                        company_funding_rounds_attributes:
                          type: array
                          description: Company funding rounds for the organization
                          items:
                            type: object
                            properties:
                              id:
                                type: string
                                description: ID of an existing funding round (for
                                  updates)
                              position:
                                type: integer
                              close_date:
                                type: string
                                description: Close date (YYYY-MM-DD)
                              investment:
                                type: number
                              round:
                                type: string
                                enum:
                                - pre_seed
                                - accelerator
                                - angel
                                - seed
                                - a
                                - b
                                - c
                                - d
                                - e
                                - f
                                - mezzanine
                              amount_raised:
                                type: number
                              security:
                                type: string
                                enum:
                                - debt
                                - convertible_debt
                                - safe
                                - common_stock
                                - preferred_stock
                                - non_dilutive_grant
                                - partnership_interest
                                - membership_interest
                                - warrants
                                - common_units
                                - preferred_units
                                - saft
                                - token
                              valuation_cap:
                                type: number
                              price_per_share:
                                type: number
                              valuation_type:
                                type: string
                                enum:
                                - pre_money_valuation
                                - post_money_valuation
                              _destroy:
                                type: boolean
                                description: Set true to remove an existing funding
                                  round
                  required:
                  - organization_id
              required:
              - deal_memo
  "/api/v1/deal_memos/{id}":
    get:
      operationId: get_deal_memo
      x-agent-tool: true
      summary: Get a single Deal Memo by id
      tags:
      - Deal Memos
      description: |
        Returns the full deal memo payload — organization, counterfactual,
        links, ratings summary, and question answers — for an agent to inspect
        before acting on the memo. Attachments, due-diligence tasks, and
        comments are not included; fetch them via their own endpoints.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Deal Memo id.
        schema:
          type: string
      responses:
        '200':
          description: the deal memo
          content:
            application/json:
              examples:
                default:
                  value:
                    data:
                      id: 101
                      stage: active
                      close_type:
                      investment_type: initial
                      created_at: '2026-05-14T12:00:00Z'
                      updated_at: '2026-05-14T12:00:00Z'
                      account_review_enabled: true
                      deal_memo_config_id: 5
                      organization:
                        id: 7
                        name: Acme Inc.
                        company_url: https://acme.example.com
                      description: API created memo
                      counterfactual: What if we passed?
                      links:
                      - id: 12
                        name: Pitch deck
                        link: https://example.com/deck
                      ratings_summary:
                        average_score: 4.2
                        raters_count: 3
                      question_answers:
                      - id: 9
                        question: What is the moat?
                        answer: Network effects in vertical SaaS.
              schema:
                type: object
                required:
                - data
                properties:
                  data:
                    type: object
                    properties:
                      account_review_enabled:
                        type: boolean
                        description: Whether the caller's account has the deal memo
                          review workflow enabled (account-level flag). When false,
                          submit_for_review and close are unavailable from the web
                          UI.
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Deal Memo not found in caller's account
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
    patch:
      operationId: update_deal_memo
      x-agent-tool: true
      summary: Update a Deal Memo
      tags:
      - Deal Memos
      description: Update an existing deal memo in the caller's account. Rejects updates
        with 422 when the deal memo is closed. Does not modify ratings, answers, attachments,
        or due-diligence tasks.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: ID of the Deal Memo to update
      responses:
        '200':
          description: Deal Memo updated
          content:
            application/json:
              examples:
                ok:
                  value:
                    data:
                      id: 101
                      stage: active
                      investment_type: initial
                      close_type:
                      created_at: '2026-05-14T12:00:00Z'
                      organization_id: 7
                      organization_name: Acme Inc.
        '400':
          description: Request body missing deal_memo wrapper
        '401':
          description: Unauthorized
        '403':
          description: Caller cannot manage investments for this account
        '404':
          description: Deal Memo not found in caller's account
        '422':
          description: Validation failed, or the deal memo is closed
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                deal_memo:
                  type: object
                  properties:
                    investment_type:
                      type: string
                      enum:
                      - initial
                      - follow on
                      - secondary
                    description:
                      type: string
                    counterfactual:
                      type: string
                    links_attributes:
                      type: array
                      description: Reference links on the deal memo (provide id to
                        update or _destroy to remove)
                      items:
                        type: object
                        properties:
                          id:
                            type: string
                          name:
                            type: string
                          link:
                            type: string
                          _destroy:
                            type: boolean
                    organization_attributes:
                      type: object
                      properties:
                        id:
                          type: string
                        company_funding_rounds_attributes:
                          type: array
                          items:
                            type: object
                            properties:
                              id:
                                type: string
                              position:
                                type: integer
                              close_date:
                                type: string
                              investment:
                                type: number
                              round:
                                type: string
                                enum:
                                - pre_seed
                                - accelerator
                                - angel
                                - seed
                                - a
                                - b
                                - c
                                - d
                                - e
                                - f
                                - mezzanine
                              amount_raised:
                                type: number
                              security:
                                type: string
                                enum:
                                - debt
                                - convertible_debt
                                - safe
                                - common_stock
                                - preferred_stock
                                - non_dilutive_grant
                                - partnership_interest
                                - membership_interest
                                - warrants
                                - common_units
                                - preferred_units
                                - saft
                                - token
                              valuation_cap:
                                type: number
                              price_per_share:
                                type: number
                              valuation_type:
                                type: string
                                enum:
                                - pre_money_valuation
                                - post_money_valuation
                              _destroy:
                                type: boolean
              required:
              - deal_memo
  "/api/v1/deal_memos/{id}/submit_for_review":
    post:
      operationId: submit_deal_memo_for_review
      x-agent-tool: true
      summary: Submit a Deal Memo for review
      tags:
      - Deal Memos
      description: Submit a deal memo for review by Decile Hub. Requires the account
        to have the Deal Memo Review product enabled (returns 422 otherwise). Optionally
        attach a submission comment via `submission_comment.content`.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: ID of the Deal Memo to submit for review
      responses:
        '200':
          description: Deal Memo submitted for review
          content:
            application/json:
              examples:
                ok:
                  value:
                    data:
                      id: 101
                      stage: review
                      investment_type: initial
                      close_type:
                      created_at: '2026-05-14T12:00:00Z'
                      organization_id: 7
                      organization_name: Acme Inc.
        '401':
          description: Unauthorized
        '403':
          description: Caller cannot manage investments for this account
        '404':
          description: Deal Memo not found in caller's account
        '422':
          description: Account does not have the Deal Memo Review product enabled,
            or validation failed
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                deal_memo:
                  type: object
                  properties:
                    submission_comment:
                      type: object
                      description: Optional comment to attach with the review submission
                      properties:
                        content:
                          type: string
                          description: Submission comment text
  "/api/v1/deal_memos/{id}/close":
    post:
      operationId: close_deal_memo
      x-agent-tool: true
      summary: Close a Deal Memo
      tags:
      - Deal Memos
      description: Close a deal memo with an outcome of approve or pass. Moves the
        underlying prospect to the configured pipeline stage and dispatches the close
        notification.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: ID of the Deal Memo to close
      responses:
        '200':
          description: Deal Memo closed
          content:
            application/json:
              examples:
                ok:
                  value:
                    data:
                      id: 101
                      stage: closed
                      investment_type: initial
                      close_type: approve
                      created_at: '2026-05-14T12:00:00Z'
                      organization_id: 7
                      organization_name: Acme Inc.
        '401':
          description: Unauthorized
        '403':
          description: Caller cannot manage investments for this account
        '404':
          description: Deal Memo not found in caller's account
        '422':
          description: close_type missing or invalid, or validation failed
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                deal_memo:
                  type: object
                  properties:
                    close_type:
                      type: string
                      enum:
                      - approve
                      - pass
                      description: Outcome of the close
                  required:
                  - close_type
              required:
              - deal_memo
  "/api/v1/portfolio_companies":
    get:
      operationId: list_portfolio_companies
      x-agent-tool: true
      summary: List portfolio companies for the current account
      tags:
      - PortfolioCompanies
      description: |
        Returns portfolio companies across every fund in the current account.
        Each row is a (fund, organization) pair — the fund's stake in a single
        underlying company. Filter by `fund_id` or `organization_id`.

        Pagination is keyset — pass `pagination.next_page_token` from a response
        back as `page_token=` to fetch the next page.
      security:
      - api_key: []
      parameters:
      - name: fund_id
        in: query
        required: false
        description: Filter to a single fund_detail id (`firm_admin_fund_details.id`).
        schema:
          type: integer
      - name: organization_id
        in: query
        required: false
        description: Filter to portfolio companies for a specific organization id.
        schema:
          type: integer
      - name: page_token
        in: query
        required: false
        description: Opaque keyset cursor returned by a previous call as `pagination.next_page_token`.
        schema:
          type: string
      - name: per_page
        in: query
        required: false
        description: Page size. Capped at 100; defaults to 25.
        schema:
          type: integer
          default: 25
          maximum: 100
      - name: fields
        in: query
        required: false
        description: Comma-separated top-level fields to return. Always returns at
          least `id` and `name`. Unknown fields are ignored.
        schema:
          type: string
      responses:
        '200':
          description: paginated list of portfolio companies
          content:
            application/json:
              examples:
                default:
                  value:
                    data:
                    - id: 123
                      name: Acme Corp
                      legal_name: Acme Corporation, Inc.
                      display_legal_name: Acme Corporation, Inc.
                      organization_id: 456
                      firm_admin_fund_details_id: 7
                      entity_type: C-Corp
                      initial_investment_date: '2023-04-01'
                      investment_thesis: Strong founder, fast-growing market.
                      reserve_consumed: '25000.00'
                      board_representation:
                        board_seats: 1
                        observer_seats: 0
                        notes: Founder retains chair.
                      exit_strategy:
                        target_exit_year: 2028
                        exit_type: acquisition
                        notes: Strategic acquirers in adjacent verticals.
                      created_at: '2023-04-01T18:31:00Z'
                      updated_at: '2024-01-15T10:00:00Z'
                    pagination:
                      next_page_token: eyJpZCI6MTIzfQ
                      has_more: true
              schema:
                type: object
                required:
                - data
                - pagination
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/PortfolioCompany"
                  pagination:
                    type: object
                    properties:
                      next_page_token:
                        type: string
                        nullable: true
                      has_more:
                        type: boolean
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
  "/api/v1/portfolio_companies/{id}":
    get:
      operationId: get_portfolio_company
      x-agent-tool: true
      summary: Get a single portfolio company by id
      tags:
      - PortfolioCompanies
      description: |
        Returns one portfolio company — the same allowlisted fields surfaced by
        `GET /api/v1/portfolio_companies`.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Portfolio company id.
        schema:
          type: integer
      - name: fields
        in: query
        required: false
        description: Comma-separated top-level fields to return. Always returns at
          least `id` and `name`. Unknown fields are ignored.
        schema:
          type: string
      responses:
        '200':
          description: the portfolio company
          content:
            application/json:
              examples:
                default:
                  value:
                    id: 123
                    name: Acme Corp
                    legal_name: Acme Corporation, Inc.
                    display_legal_name: Acme Corporation, Inc.
                    organization_id: 456
                    firm_admin_fund_details_id: 7
                    entity_type: C-Corp
                    initial_investment_date: '2023-04-01'
                    investment_thesis: Strong founder, fast-growing market.
                    reserve_consumed: '25000.00'
                    board_representation:
                      board_seats: 1
                      observer_seats: 0
                      notes: Founder retains chair.
                    exit_strategy:
                      target_exit_year: 2028
                      exit_type: acquisition
                      notes: Strategic acquirers in adjacent verticals.
                    created_at: '2023-04-01T18:31:00Z'
                    updated_at: '2024-01-15T10:00:00Z'
              schema:
                "$ref": "#/components/schemas/PortfolioCompany"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/portfolio_companies/{portfolio_company_id}/investments":
    get:
      operationId: list_portfolio_company_investments
      x-agent-tool: true
      summary: List investments for a portfolio company
      tags:
      - PortfolioCompanies
      description: |
        Returns the investments (accounting-transaction-driven tranches) belonging
        to a single portfolio company. Each row includes its effective mark, linked
        funding-round security, and valuation provenance as of the selected date.
        FMV and mark price match the expanded portfolio UI tranche row in normal
        (non-cap-adjusted) mode. Tranches written off by that date, and tranches
        whose investment transaction is dated after it, are omitted.

        Pagination is keyset — pass `pagination.next_page_token` from a response
        back as `page_token=` to fetch the next page.
      security:
      - api_key: []
      parameters:
      - name: portfolio_company_id
        in: path
        required: true
        description: Portfolio company id.
        schema:
          type: integer
      - name: as_of_date
        in: query
        required: false
        description: ISO date used to resolve each tranche current mark. Valuations
          effective after this date are excluded. Defaults to today.
        schema:
          type: string
          format: date
      - name: page_token
        in: query
        required: false
        description: Opaque keyset cursor returned by a previous call as `pagination.next_page_token`.
        schema:
          type: string
      - name: per_page
        in: query
        required: false
        description: Page size. Capped at 100; defaults to 25.
        schema:
          type: integer
          default: 25
          maximum: 100
      - name: fields
        in: query
        required: false
        description: Comma-separated top-level fields to return. Always returns at
          least `id`. Unknown fields are ignored.
        schema:
          type: string
      responses:
        '200':
          description: paginated list of investments
          content:
            application/json:
              examples:
                default:
                  value:
                    data:
                    - id: 901
                      firm_admin_portfolio_companies_id: 123
                      firm_admin_accounting_transactions_id: 4567
                      shares: 1000
                      shares_decimal: '1000.0'
                      ignore_for_fmv_calc: false
                      current_fmv: '1234.567'
                      current_mark_price_per_share: '1.234567'
                      security_type: preferred_stock
                      security_series: a
                      company_funding_round_id: 789
                      valuation_event_id: 987
                      fmv_source_type: valuation_event
                      fmv_as_of_date: '2023-12-31'
                      valuation_basis: price_per_share
                      tax_born_on: '2023-04-01'
                      created_at: '2023-04-01T18:31:00Z'
                      updated_at: '2024-01-15T10:00:00Z'
                    pagination:
                      next_page_token: eyJpZCI6OTAxfQ
                      has_more: true
              schema:
                type: object
                required:
                - data
                - pagination
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/PortfolioCompanyInvestment"
                  pagination:
                    type: object
                    properties:
                      next_page_token:
                        type: string
                        nullable: true
                      has_more:
                        type: boolean
        '400':
          description: Invalid parameter
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Portfolio company not found in current account
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/portfolio_companies/{portfolio_company_id}/valuations":
    get:
      operationId: list_portfolio_company_valuations
      x-agent-tool: true
      summary: List valuation history for a portfolio company
      tags:
      - PortfolioCompanies
      description: |
        Returns valuation events for one portfolio company, ordered by valuation
        date newest first with an id tiebreaker. Numeric valuation values are strings.

        Pagination is keyset — pass `pagination.next_page_token` from a response
        back as `page_token=` to fetch the next page.
      security:
      - api_key: []
      parameters:
      - name: portfolio_company_id
        in: path
        required: true
        description: Portfolio company id.
        schema:
          type: integer
      - name: as_of_date
        in: query
        required: false
        description: Return valuations on or before this date.
        schema:
          type: string
          format: date
      - name: page_token
        in: query
        required: false
        description: Opaque keyset cursor returned by a previous call as `pagination.next_page_token`.
        schema:
          type: string
      - name: per_page
        in: query
        required: false
        description: Page size. Capped at 100; defaults to 25.
        schema:
          type: integer
          default: 25
          maximum: 100
      - name: fields
        in: query
        required: false
        description: Comma-separated summary fields to return. Always returns at least
          `id`. Unknown fields are ignored.
        schema:
          type: string
      responses:
        '200':
          description: paginated valuation history
          content:
            application/json:
              examples:
                default:
                  value:
                    data:
                    - id: 902
                      valuation_date: '2026-01-15'
                      valuation_type: mark_up
                      reason: business_performance
                      mark_amount: '2500000.00'
                      price_per_share: '2.5000000000'
                      shares_outstanding: '1000000'
                      approved: true
                      audited: true
                      company_funding_round_id: 456
                      created_at: '2026-01-15T18:31:00Z'
                      updated_at: '2026-01-16T10:00:00Z'
                    pagination:
                      next_page_token: eyJ2YWx1YXRpb25fZGF0ZSI6IjIwMjYtMDEtMTUiLCJpZCI6OTAyfQ
                      has_more: true
              schema:
                type: object
                required:
                - data
                - pagination
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/PortfolioCompanyValuationSummary"
                  pagination:
                    type: object
                    properties:
                      next_page_token:
                        type: string
                        nullable: true
                      has_more:
                        type: boolean
        '400':
          description: Invalid `as_of_date` or `page_token`.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Portfolio company not found in current account
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/entities/{entity_id}/schedule_of_investments":
    get:
      operationId: list_schedule_of_investments
      x-agent-tool: true
      summary: List a structured Schedule of Investments
      tags:
      - Entities
      description: |
        Returns the same canonical Schedule of Investments calculations used by
        Hub's CSV, XLSX, PDF, and UI reporting as machine-readable JSON. The
        response groups positions by portfolio company and includes position
        marks, security and funding-round details, valuation provenance,
        company subtotals, and report-wide totals.

        This is a synchronous, read-only reconciliation endpoint. Use
        `generate_financial_report` when a downloadable report file is needed.
        Pagination applies to company groups; metadata and grand totals always
        describe the complete report.
      security:
      - api_key: []
      parameters:
      - name: entity_id
        in: path
        required: true
        description: Fund, SPV, or holding entity id in the current account.
        schema:
          type: integer
      - name: as_of_date
        in: query
        required: true
        description: Inclusive report cutoff date in ISO YYYY-MM-DD format.
        schema:
          type: string
          format: date
      - name: valuation_basis
        in: query
        required: false
        description: Valuation basis. Defaults to the canonical GAAP SOI basis.
        schema:
          type: string
          enum:
          - gaap
          default: gaap
      - name: page_token
        in: query
        required: false
        description: Opaque company-group cursor from `pagination.next_page_token`.
        schema:
          type: string
      - name: per_page
        in: query
        required: false
        description: Company groups per page. Defaults to 10 and is capped at 25.
        schema:
          type: integer
          default: 10
          maximum: 25
      - name: fields
        in: query
        required: false
        description: Comma-separated position fields. `position_id` is always returned.
        schema:
          type: string
      responses:
        '200':
          description: structured Schedule of Investments
          content:
            application/json:
              schema:
                type: object
                required:
                - metadata
                - companies
                - grand_total
                - pagination
                properties:
                  metadata:
                    "$ref": "#/components/schemas/ScheduleOfInvestmentsMetadata"
                  companies:
                    type: array
                    items:
                      "$ref": "#/components/schemas/ScheduleOfInvestmentsCompany"
                  grand_total:
                    "$ref": "#/components/schemas/ScheduleOfInvestmentsTotals"
                  pagination:
                    type: object
                    required:
                    - per_page
                    - next_page_token
                    - has_more
                    properties:
                      per_page:
                        type: integer
                      next_page_token:
                        type: string
                        nullable: true
                      has_more:
                        type: boolean
              examples:
                default:
                  value:
                    metadata:
                      entity_id: 42
                      entity_kind: fund
                      entity_name: Example Fund I
                      as_of_date: '2025-12-31'
                      valuation_basis: gaap
                      company_count: 1
                      position_count: 1
                    companies:
                    - company_id: 91
                      name: Example Company
                      positions:
                      - position_id: 301
                        company_id: 91
                        effective_date: '2024-06-01'
                        funding_round_id: 55
                        funding_round_name: Series A
                        security_name: Priced
                        security_type: equity
                        security_series: Series A
                        shares: '100000'
                        cost_basis: '500000'
                        current_fmv: '750000'
                        cost_per_share: '5'
                        mark_price_per_share: '7.5'
                        unrealized_gain_loss: '250000'
                        excluded_from_totals: false
                        valuation_event:
                          id: 801
                          date: '2025-12-31'
                          source: Recent funding round
                          synthetic: false
                          funding_round_id: 55
                          funding_round_name: Series A
                      subtotal:
                        shares: '100000'
                        cost_basis: '500000'
                        current_fmv: '750000'
                        unrealized_gain_loss: '250000'
                    grand_total:
                      shares: '100000'
                      cost_basis: '500000'
                      current_fmv: '750000'
                      unrealized_gain_loss: '250000'
                    pagination:
                      per_page: 10
                      next_page_token:
                      has_more: false
        '400':
          description: Invalid or missing `as_of_date`, unsupported `valuation_basis`,
            or invalid `page_token`.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Entity is not a fund, SPV, or holding in the current account
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/portfolio_company_valuations/{id}":
    get:
      operationId: get_portfolio_company_valuation
      x-agent-tool: true
      summary: Get a portfolio company valuation event
      tags:
      - PortfolioCompanies
      description: |
        Returns the complete tenant-scoped provenance for one valuation event.
        Numeric valuation and funding-round values are serialized as strings.
        The linked round id is returned by default; pass `include=funding_rounds`
        to embed all authorized rounds for the valuation company, with terms and
        document references ordered newest first. Unknown
        include values are ignored.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Portfolio company valuation id.
        schema:
          type: integer
      - name: include
        in: query
        required: false
        description: Comma-separated expansions. Use `funding_rounds` to embed all
          funding rounds for the valuation company.
        schema:
          type: string
          example: funding_rounds
      responses:
        '200':
          description: valuation event provenance
          content:
            application/json:
              examples:
                default:
                  value:
                    id: 902
                    portfolio_company_id: 123
                    organization_id: 456
                    valuation_date: '2026-01-15'
                    valuation_type: mark_up
                    reason: funding_round
                    notes: Series A mark based on the signed round documents
                    mark_amount: '2500000.0'
                    exit_amount:
                    price_per_share: '2.5'
                    shares_outstanding: '1000000'
                    valuation_cap: '10000000'
                    repurchase_shares:
                    repurchase_pps:
                    override_fmv_share_price_with_cost_basis_share_price: false
                    cap_adjusted_discount:
                    cap_adjusted_price_per_share:
                    cap_adjusted_valuation_cap:
                    approved: true
                    audited: true
                    audited_at: '2026-01-16T10:00:00Z'
                    creator:
                      id: 12
                      name: Fund Owner
                      email: owner@example.com
                    auditor:
                      id: 13
                      name: Fund Admin
                      email: admin@example.com
                    company_funding_round_id: 4567
                    attachments:
                    - id: 81
                      name: valuation-memo.pdf
                    created_at: '2026-01-15T18:31:00Z'
                    updated_at: '2026-01-16T10:00:00Z'
                withFundingRound:
                  value:
                    id: 902
                    portfolio_company_id: 123
                    organization_id: 456
                    valuation_date: '2026-01-15'
                    valuation_type: investment
                    reason: funding_round
                    company_funding_round_id: 4567
                    funding_rounds:
                    - id: 4568
                      round: b
                      close_date: '2026-06-15'
                      security: preferred_stock
                      unit_class: Series B Preferred
                      price_per_share: '4.0'
                      amount_raised: '5000000.0'
                      currency: USD
                      pre_money_valuation: '20000000'
                      post_money_valuation: '25000000'
                      valuation_cap:
                      valuation_type: post_money_valuation
                      converted_to:
                      converted_when:
                      discount:
                      cap_adjusted_discount:
                      cap_adjusted_price_per_share:
                      cap_adjusted_valuation_cap:
                      approved: true
                      open: false
                      attachments: []
                    - id: 4567
                      round: a
                      close_date: '2026-01-15'
                      security: preferred_stock
                      unit_class: Series A Preferred
                      price_per_share: '2.5'
                      amount_raised: '3000000.0'
                      currency: USD
                      pre_money_valuation: '10000000'
                      post_money_valuation: '13000000'
                      valuation_cap:
                      valuation_type: post_money_valuation
                      converted_to:
                      converted_when:
                      discount:
                      cap_adjusted_discount:
                      cap_adjusted_price_per_share:
                      cap_adjusted_valuation_cap:
                      approved: true
                      open: false
                      attachments:
                      - id: 82
                        name: series-a-stock-purchase-agreement.pdf
              schema:
                "$ref": "#/components/schemas/PortfolioCompanyValuationDetail"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Valuation not found in current account
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/portfolio_company_investments/{id}":
    get:
      operationId: get_portfolio_company_investment
      x-agent-tool: true
      summary: Get a single portfolio company investment
      tags:
      - PortfolioCompanies
      description: |
        Returns a single investment tranche (accounting-transaction-driven) on a portfolio
        company, with applied overrides resolved.

        The response combines the column allowlist exposed by
        `list_portfolio_company_investments` (id, parent FKs, shares, shares_decimal,
        ignore_for_fmv_calc, tax_born_on, timestamps) with a fixed set of computed/resolved
        fields: `effective_shares`, `effective_tax_born_date`, `price_per_share`,
        `transaction_amount` (cost basis with write-off and convertible-debt conversion
        applied), and `transaction_at`. The reconciliation fields match the collection
        endpoint for the same investment and selected as-of date. `mark_provenance`
        identifies the exact valuation, override, or cost-basis source record.

        Raw admin levers (override_fmv*, override_investment_on, legal_cost_per_share,
        write_off_investment*, convertible_debt_new_cost) are intentionally hidden — the
        computed fields encode their resolved effect.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Portfolio company investment id.
        schema:
          type: integer
      - name: fields
        in: query
        required: false
        description: Comma-separated top-level fields to return, including reconciliation
          fields `current_fmv`, `current_mark_price_per_share`, `security_type`, `security_series`,
          `company_funding_round_id`, `valuation_event_id`, `fmv_source_type`, `fmv_as_of_date`,
          `valuation_basis`, and `mark_provenance`. Always returns at least `id`.
          Unknown fields are ignored.
        schema:
          type: string
      - name: as_of_date
        in: query
        required: false
        description: ISO date used to resolve the current mark. Valuations effective
          after this date are excluded. Defaults to today.
        schema:
          type: string
          format: date
      responses:
        '200':
          description: a single investment with applied overrides resolved
          content:
            application/json:
              examples:
                default:
                  value:
                    id: 901
                    firm_admin_portfolio_companies_id: 123
                    firm_admin_accounting_transactions_id: 4567
                    shares: 1000
                    shares_decimal: '1000.0'
                    ignore_for_fmv_calc: false
                    tax_born_on: '2023-04-01'
                    created_at: '2023-04-01T18:31:00Z'
                    updated_at: '2024-01-15T10:00:00Z'
                    effective_shares: '1000.0'
                    effective_tax_born_date: '2023-04-01'
                    price_per_share: '100.0'
                    transaction_amount: '100000.0'
                    transaction_at: '2023-04-01T18:31:00Z'
                    current_fmv: '250000.0'
                    current_mark_price_per_share: '250.0'
                    security_type: preferred_stock
                    security_series: Series A
                    company_funding_round_id: 88
                    valuation_event_id: 99
                    fmv_source_type: valuation_event
                    fmv_as_of_date: '2024-01-15'
                    valuation_basis: price_per_share
                    mark_provenance:
                      source_type: valuation_event
                      source_record_id: 99
                      effective_date: '2024-01-15'
                      valuation_basis: price_per_share
                      valuation_event_id: 99
                      company_funding_round_id: 88
              schema:
                "$ref": "#/components/schemas/PortfolioCompanyInvestmentDetail"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Investment not found in current account
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '400':
          description: Invalid parameter
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/pacts":
    get:
      operationId: list_pacts
      x-agent-tool: true
      summary: List signed PACTs (soft commitments)
      tags:
      - Pacts
      description: |
        Lists signed PACTs (pledge agreements capturing an investor's soft
        commitment / intended investment amount) on the caller account's
        investor pipelines. This is the endpoint that answers "has NAME signed
        a PACT?" — filter by `q` (fuzzy signatory name), `person_id`,
        `pipeline_id`, or a `signed_after`/`signed_before` date window.

        Results are scoped to investor pipelines the caller can access,
        paginated 50 rows per page, ordered newest `signed_on` first by
        default. `id` and `pipeline_id` are obscure ids; `person_id` is the
        raw numeric Person id — the same form list_people and get_prospects
        emit and send_pact/get_person accept, so it round-trips directly into
        subsequent tool calls. `person_id` may be null for PACTs uploaded
        without a linked Person. `is_manual_upload: true` marks documents
        uploaded by firm staff rather than signed online. Rows with
        `ignore_for_reporting: true` are excluded from firm reporting but are
        still returned here so agents see the complete picture.

        Alongside `pagination.total_count`, the response carries aggregates
        computed across EVERY row matching the current filters, not just the
        page: `total_pledged_amount_all_matches` (sum of `investment`),
        `total_pledged_amount_reportable` (the same sum excluding
        `ignore_for_reporting` rows), and `reportable_count`. Quote these
        verbatim — never sum `investment` across pages by hand. Pair
        `total_pledged_amount_reportable` with `get_entity`'s
        `target_fund_size` to answer "what percent of my fund size is in
        PACTs?". The sum uses the same leading-integer semantics as
        `Account#reporting_pact_sum`, so unparseable amounts count as 0.
      security:
      - api_key: []
      parameters:
      - name: q
        in: query
        required: false
        description: Fuzzy signatory name match (first, last, or full name; case-insensitive
          contains).
        schema:
          type: string
      - name: person_id
        in: query
        required: false
        description: Filter by the Person who signed (numeric id as returned by list_people/get_prospects;
          obscure ids also accepted).
        schema:
          type: string
      - name: pipeline_id
        in: query
        required: false
        description: Filter by investor pipeline (obscure id).
        schema:
          type: string
      - name: signed_after
        in: query
        required: false
        description: Only PACTs signed on/after this date (YYYY-MM-DD).
        schema:
          type: string
      - name: signed_before
        in: query
        required: false
        description: Only PACTs signed on/before this date (YYYY-MM-DD).
        schema:
          type: string
      - name: page
        in: query
        required: false
        description: Page number (0-indexed, 50 rows per page).
        schema:
          type: integer
      - name: order_by
        in: query
        required: false
        description: Field to order by. Defaults to signed_on.
        schema:
          type: string
          enum:
          - signed_on
          - last_name
          - created_at
      - name: order_direction
        in: query
        required: false
        description: Order direction (asc or desc). Defaults to desc for signed_on,
          asc otherwise.
        schema:
          type: string
          enum:
          - asc
          - desc
      responses:
        '200':
          description: signed PACTs matching the filters
          content:
            application/json:
              examples:
                default:
                  value:
                    data:
                    - id: yD3mGkKk
                      person_id: 4321
                      pipeline_id: zX8cVbNm
                      first_name: Mike
                      middle_name:
                      last_name: Parker
                      invest_as: individual
                      investment: '100000'
                      signed_on: '2026-02-07T12:00:00.000Z'
                      created_at: '2026-02-07T12:00:00.000Z'
                      is_manual_upload: false
                      ignore_for_reporting: false
                    pagination:
                      total_count: 1
                      current_page: 0
                      total_pages: 1
                    reportable_count: 1
                    total_pledged_amount_all_matches: '100000.0'
                    total_pledged_amount_reportable: '100000.0'
        '400':
          description: Invalid filter (e.g. malformed signed_after/signed_before date)
            or invalid order parameters
        '401':
          description: Unauthorized
        '403':
          description: Caller lacks access to any investor pipeline
  "/api/v1/pacts/send_pact":
    post:
      operationId: send_pact
      x-agent-tool: true
      summary: Send a PACT email to a Person
      tags:
      - Pacts
      description: Mirrors the UI "Send PACT" button. Adds or moves the Person onto
        the investor pipeline's Send PACT stage, finds the configured PACT email template,
        and sends it. `pipeline_id` is required only when the account has more than
        one investor pipeline.
      security:
      - api_key: []
      parameters: []
      responses:
        '200':
          description: PACT email dispatched
          content:
            application/json:
              examples:
                ok:
                  value:
                    success: true
                    pipeline_prospect_id: 1234
                    pipeline_action_execution_id: 5678
        '400':
          description: pipeline_id required when multiple investor pipelines exist
        '401':
          description: Unauthorized
        '403':
          description: Caller lacks access to the investor pipeline
        '404':
          description: Person or pipeline not found
        '422':
          description: Pipeline is not an investor pipeline, or PACT stage / email
            template not configured
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                person_id:
                  type: string
                  description: Person ID in caller account
                pipeline_id:
                  type: string
                  description: Optional investor pipeline obscure id
              required:
              - person_id
  "/api/v1/search/people_orgs":
    get:
      operationId: search_people_orgs
      x-agent-tool: true
      summary: Search people and organizations by name, email, or company fragment
      tags:
      - Search
      description: |
        Combined lookup across the caller account's People and Organizations —
        the endpoint that answers "who/what is X?" and "who referred us to X?".
        Matching is anchored: every word of `q` must start a word in the
        person's first/last name or email, or the organization's name or
        company URL, case- and accent-insensitively. So "rui@" and "jake kel"
        still hit, "munoz" finds "Muñoz", and "jake kelley" matches in either
        order — but a mid-word fragment like "ell" no longer drags in
        "Winkelman". Trigram fuzzy matching only backfills when the anchored
        pass returns less than a full page; those rows carry `match: "fuzzy"`
        (a phonetic guess, not a confirmed name) while anchored rows carry
        `match: "strong"`. Confirm a fuzzy hit with the user before acting on
        it. Person and organization hits are interleaved so neither type
        starves the other when a bare name matches both (e.g. an eponymous
        founder and their company).

        Results are strictly scoped to the caller's account and paginated 15
        rows per page. Relevance is bounded: at most 60 matches per record
        type are considered, so `matched_count` is the number of matched
        records this query returned (capped at 120), NOT the account total —
        narrow the query rather than paging deep, and use `count_account_records`
        for a true account-wide total. Person rows carry `email`, `phone`,
        `organizations` (org affiliations with titles, e.g. "Acme (CEO)"),
        `referred_by` (full name of the Person who referred them, null when
        nobody did), and a direct Hub `url`. Organization rows carry
        `website`, `lifecycle_stage`, `contacts` (up to 5 linked people with
        titles), `referred_by`, free-text `referral_source`, and a direct Hub
        `url`. The `referred_by` field is the answer to "who referred us to
        X". Requires read access to both people and organizations.
      security:
      - api_key: []
      parameters:
      - name: q
        in: query
        required: true
        description: Name, email fragment, or company-name fragment to look up (e.g.
          "jake kelley", "rui@", "Acme").
        schema:
          type: string
      - name: kind
        in: query
        required: false
        description: Restrict results to one record type. Defaults to both.
        schema:
          type: string
          enum:
          - person
          - organization
          - both
      - name: page
        in: query
        required: false
        description: Page number (0-indexed, 15 rows per page).
        schema:
          type: integer
      responses:
        '200':
          description: people and organizations matching the query
          content:
            application/json:
              examples:
                default:
                  value:
                    data:
                    - type: person
                      match: strong
                      id: 4321
                      name: Jake Kelley
                      email: jake@acme.com
                      phone: 555-0101
                      organizations:
                      - Acme Ventures (Partner)
                      referred_by: Rui Cao
                      url: https://myfirm.decilehub.com/people?open_edit=4321
                    - type: organization
                      match: strong
                      id: 987
                      name: Acme Ventures
                      website: https://acme.example
                      lifecycle_stage: Sourcing
                      contacts:
                      - Jake Kelley (Partner)
                      referred_by: Rui Cao
                      referral_source: Warm intro from portfolio founder
                      url: https://myfirm.decilehub.com/organizations?open_edit=987
                    pagination:
                      matched_count: 2
                      current_page: 0
                      total_pages: 1
        '400':
          description: Missing q or invalid kind/page parameter
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '403':
          description: Caller lacks read access to people or organizations
  "/api/v1/search/pipeline_prospects":
    get:
      operationId: search_pipeline_prospects
      x-agent-tool: true
      summary: Search pipeline prospects (investor/LP and deal-flow) by name or email
      tags:
      - Search
      description: |
        Prospect lookup that answers "is <name> in my pipeline?", "find the
        <company> deal", and "what stage is <LP name> at?".

        SCOPE - read this before concluding a prospect is "already in" a
        pipeline. Without `pipeline_id` the search spans EVERY pipeline the
        caller can read, so a hit only proves the prospect exists somewhere in
        the account. To answer "is X already in pipeline P?", pass
        `pipeline_id: P` (or check each row's `pipeline.id`). Never treat an
        unscoped hit as proof of membership in a particular pipeline.
        `pagination.pipeline_scoped` echoes which mode ran.

        Matching is anchored: every word of `q` must start a word in the
        prospect's denormalized name/email, case- and accent-insensitively, so
        "kel" finds "Kelley" but not "Winkelman" and "jake kelley" matches in
        either order. Trigram fuzzy matching only backfills when the anchored
        pass returns less than a full page; those rows come back with
        `match: "fuzzy"` (a phonetic guess, not a confirmed name) while
        anchored rows carry `match: "strong"`.

        Results cover Person and Organization prospects in active
        (non-archived) pipelines — investor/fundraising AND deal-flow
        (`investment`) pipelines alike — and are strictly scoped to the
        caller's account and per-pipeline permissions (a member without access
        to a pipeline type never sees its prospects). Rows are paginated 15
        per page; at most 60 matches are considered, so
        `pagination.matched_count` is a bounded match count, NOT an account
        total. Each row carries `name`, `prospect_type` (person or
        organization), `email`, `probability`, `capital_commitment`, its
        `pipeline` {id, name, type} and `stage` {id, name}, `match`, and a
        direct Hub `url` deep-link that opens the prospect in its pipeline —
        include that link when reporting the hit. `pipeline.type`
        distinguishes investor pipelines from deal-flow (`investment`)
        pipelines.
      security:
      - api_key: []
      parameters:
      - name: q
        in: query
        required: true
        description: Prospect name, email fragment, or company-name fragment to look
          up (e.g. "jake kelley", "Acme").
        schema:
          type: string
      - name: pipeline_id
        in: query
        required: false
        description: Restrict the search to a single pipeline (obscure id from pipeline
          listings; numeric also accepted). Required to answer "is X already in THIS
          pipeline?" - without it the search spans every readable pipeline. 404s when
          the pipeline is unknown or outside the caller's readable set.
        schema:
          type: string
      - name: page
        in: query
        required: false
        description: Page number (0-indexed, 15 rows per page).
        schema:
          type: integer
      responses:
        '200':
          description: pipeline prospects matching the query
          content:
            application/json:
              examples:
                default:
                  value:
                    data:
                    - type: prospect
                      id: 4321
                      name: Jake Kelley
                      prospect_type: person
                      email: jake@acme.com
                      probability: 50
                      capital_commitment: 250000
                      pipeline:
                        id: aB3xYz
                        name: Investors
                        type: investor
                      stage:
                        id: 246
                        name: Soft Circled
                      match: strong
                      url: https://myfirm.decilehub.com/people/xY12ab/edit_prospect
                    - type: prospect
                      id: 987
                      name: Acme Robotics
                      prospect_type: organization
                      email:
                      probability: 80
                      capital_commitment:
                      pipeline:
                        id: cD4wVu
                        name: Deals
                        type: investment
                      stage:
                        id: 310
                        name: Diligence
                      match: fuzzy
                      url: https://myfirm.decilehub.com/organizations/zZ34cd/edit_prospect
                    pagination:
                      matched_count: 2
                      current_page: 0
                      total_pages: 1
                      pipeline_scoped: false
        '400':
          description: Missing q parameter
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '404':
          description: pipeline_id is unknown or outside the caller's readable pipelines
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/counts":
    get:
      operationId: count_account_records
      x-agent-tool: true
      x-agent-category: context
      summary: Count account-wide records by resource, with optional filters
      tags:
      - Counts
      description: |
        Real, filterable COUNT(*) for a single resource on the caller's
        account — the tool that answers "how many people / organizations /
        entities / capital accounts / capital calls / PACTs do we have?".
        Unlike search_people_orgs (whose `matched_count` is a fuzzy match count
        capped at 120), this returns an exact account-wide total.

        `resource` (required) selects what to count. `entity_id` narrows
        `capital_accounts` / `capital_calls` to one fund; omit it for an
        account-wide fan-out across every fund the caller can access — the
        response then carries a `breakdown` per fund plus `funds_counted` /
        `funds_total` so a shrunk denominator is never quoted as the account
        total (when `funds_counted < funds_total`, say "across N of M funds you
        can access"). Optional per-resource filters apply on top of the
        authorization-scoped relation: `q` (people/organizations name/email
        substring), `type` (entities STI type, e.g. AccountOrganizationFund),
        `status` (capital_calls: draft/open/closed). A bare `people` /
        `organizations` count returns every contact (including imported and
        prospect records), not just active relationships.
      security:
      - api_key: []
      parameters:
      - name: resource
        in: query
        required: true
        description: Which resource to count.
        schema:
          type: string
          enum:
          - people
          - organizations
          - entities
          - capital_accounts
          - capital_calls
          - pacts
      - name: entity_id
        in: query
        required: false
        description: Narrow capital_accounts/capital_calls to one fund entity. Omit
          for account-wide fan-out.
        schema:
          type: integer
      - name: q
        in: query
        required: false
        description: Name/email substring filter (people, organizations only).
        schema:
          type: string
      - name: type
        in: query
        required: false
        description: Entity STI type filter (entities only), e.g. AccountOrganizationFund.
        schema:
          type: string
      - name: status
        in: query
        required: false
        description: Capital call status filter (capital_calls only).
        schema:
          type: string
          enum:
          - draft
          - open
          - closed
      responses:
        '200':
          description: the count for the requested resource
          content:
            application/json:
              examples:
                default:
                  value:
                    resource: capital_accounts
                    count: 412
                    scope:
                      entity_id:
                      filters: {}
                      funds_counted: 2
                      funds_total: 3
                    breakdown:
                    - entity_id: 123
                      entity_name: Fund I
                      count: 300
                    - entity_id: 456
                      entity_name: Fund II
                      count: 112
        '400':
          description: Missing or invalid resource parameter
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '401':
          description: Unauthorized
        '403':
          description: Caller lacks read access to the requested resource
        '404':
          description: entity_id does not resolve to an entity on the caller's account
  "/api/v1/lpas/send_lpa":
    post:
      operationId: send_lpa
      x-agent-tool: true
      summary: Send an LPA email to a Person
      tags:
      - Lpas
      description: Mirrors the UI "Send LPA" button. Resolves the target investor
        pipeline and its linked closing pipeline, moves the Person onto the investor
        pipeline's Closing stage, creates or finds their capital account on the fund,
        seats the closing-pipeline prospect on the Send LPA / Send questionnaire stage,
        finds the configured LPA email template, and sends it. `pipeline_id` is required
        only when the account has more than one investor pipeline. The returned `pipeline_prospect_id`
        refers to the closing-pipeline prospect (capital account), not the investor-pipeline
        prospect.
      security:
      - api_key: []
      parameters: []
      responses:
        '200':
          description: LPA email dispatched
          content:
            application/json:
              examples:
                ok:
                  value:
                    success: true
                    pipeline_prospect_id: 9012
                    pipeline_action_execution_id: 3456
        '400':
          description: pipeline_id required when multiple investor pipelines exist
        '401':
          description: Unauthorized
        '403':
          description: Caller lacks access to the investor pipeline
        '404':
          description: Person or pipeline not found
        '422':
          description: Pipeline is not an investor pipeline; no closing pipeline /
            fund linked; no LPA email template or target stage configured
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                person_id:
                  type: string
                  description: Person ID in caller account
                pipeline_id:
                  type: string
                  description: Optional investor pipeline obscure id
              required:
              - person_id
  "/api/v1/base/inbox":
    get:
      operationId: list_base_inbox
      x-agent-tool: true
      summary: List Decile Base inbox items
      tags:
      - Base
      description: Returns the authenticated user's Decile Base inbox — a paginated,
        time-ordered feed of posts and replies they are subscribed to or mentioned
        in. Available to any API key with Base access. When `channel_id` is supplied,
        the API key's user must have access to that channel.
      security:
      - api_key: []
      parameters:
      - name: page
        in: query
        description: Page number (1-indexed)
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        description: Items per page (1–100, default 50)
        schema:
          type: integer
          default: 50
          minimum: 1
          maximum: 100
      - name: channel_id
        in: query
        description: Restrict results to a single Base channel ID
        schema:
          type: integer
      - name: exclude_channel_ids
        in: query
        description: Comma-separated list of channel IDs to exclude
        schema:
          type: string
          example: '12,34,56'
      - name: since
        in: query
        description: ISO 8601 datetime — only items created/updated on or after this
          time
        schema:
          type: string
          format: date-time
          example: '2026-04-01T00:00:00Z'
      - name: user_ids
        in: query
        description: Comma-separated list of user IDs to filter by author
        schema:
          type: string
          example: '1,2,3'
      responses:
        '200':
          description: inbox returned
          content:
            application/json:
              examples:
                success:
                  value:
                    items:
                    - type: post
                      id: 101
                      post_id: 555
                      channel_id: 8
                      channel_name: decile-base
                      channel_type: Base::ConversationChannel
                      user:
                        id: 7
                        first_name: John
                        last_name: Doe
                        email: john@example.com
                      title: Welcome to Base
                      content: Hello world
                      url: https://www.decilehub.com/base/8/posts/555
                      attachments: []
                      unread: true
                      created_at: '2026-04-29T15:30:00Z'
                      updated_at: '2026-04-29T15:30:00Z'
                      replies_count: 3
                    meta:
                      page: 1
                      per_page: 50
                      total: 1
                      has_more: false
        '400':
          description: bad request (e.g. invalid `since` parameter)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                bad_since:
                  value:
                    error:
                      code: bad_request
                      message: Invalid since parameter. Use ISO 8601 format.
        '401':
          description: Unauthorized — API key missing, invalid, or expired
        '403':
          description: Forbidden — API key lacks necessary Base access
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/base/channels":
    get:
      operationId: list_base_channels
      x-agent-tool: true
      summary: List accessible Decile Base channels
      tags:
      - Base
      description: Lists Base channels the authenticated user can see. Available to
        any API key with Base access. Returns the user's accessible question and conversation
        channels.
      security:
      - api_key: []
      parameters:
      - name: exclude_channel_ids
        in: query
        description: Comma-separated list of channel IDs to exclude from the response
        schema:
          type: string
          example: '12,34'
      responses:
        '200':
          description: channels returned
          content:
            application/json:
              examples:
                success:
                  value:
                    channels:
                    - id: 8
                      name: decile-base
                      friendly_name: Decile Base
                      type: Base::ConversationChannel
                      public: true
                      sensitive: false
                      parent_channel_id:
                      posts_count: 142
                      color: "#3366FF"
                    - id: 12
                      name: questions
                      friendly_name: Questions
                      type: Base::QuestionChannel
                      public: true
                      sensitive: false
                      parent_channel_id:
                      posts_count: 37
                      color: "#22AA88"
        '401':
          description: Unauthorized
        '403':
          description: Forbidden — API key lacks Base access
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/base/channels/{id}/posts":
    get:
      operationId: list_base_channel_posts
      x-agent-tool: true
      summary: List posts in a Decile Base channel
      tags:
      - Base
      description: Lists posts in a single Base channel, most-recent-first, with pagination.
        Available to any API key with Base access; the acting user must also be authorized
        for the channel. Posts whose author chose to remain anonymous are returned
        with a neutral author ("Community Member"); the real user id, name, email,
        and authority are withheld unless the caller is permitted to see that profile.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Base channel ID
        schema:
          type: integer
      - name: page
        in: query
        description: Page number (1-indexed)
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        description: Posts per page (1..100)
        schema:
          type: integer
          default: 50
      responses:
        '200':
          description: posts returned
          content:
            application/json:
              examples:
                success:
                  value:
                    posts:
                    - id: 555
                      title: Welcome to Base
                      created_at: '2026-04-29T15:30:00Z'
                      updated_at: '2026-04-29T15:30:00Z'
                      channel_id: 8
                      content: Hello world
                      url: https://www.decilehub.com/base/8/posts/555
                      replies_count: 3
                      anonymous: false
                      user_id: 7
                      author_name: John Doe
                      author_authority: expert
                      user:
                        id: 7
                        first_name: John
                        last_name: Doe
                        email: john@example.com
                    - id: 556
                      title: A question posted anonymously
                      created_at: '2026-04-28T09:00:00Z'
                      updated_at: '2026-04-28T09:00:00Z'
                      channel_id: 8
                      content: Asking for a friend
                      url: https://www.decilehub.com/base/8/posts/556
                      replies_count: 0
                      anonymous: true
                      user_id:
                      author_name: Community Member
                      author_authority:
                      user:
                    meta:
                      page: 1
                      per_page: 50
                      total: 2
                      has_more: false
        '401':
          description: Unauthorized
        '403':
          description: Forbidden — API key cannot access this channel
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: channel not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/base/posts":
    post:
      operationId: create_base_post
      x-agent-tool: true
      summary: Create a Decile Base post
      tags:
      - Base
      description: |-
        Creates a new post in a Base channel. The `content` is parsed as Markdown and stored as rich text. Available to any API key with Base access; the acting user must also be authorized for the target channel. The post is always attributed to the user who owns the API key; impersonation is not supported.

        `@FirstName` and `@FirstName LastName` tokens in `content` become real mentions, notifying the mentioned user. A token resolves only when it matches exactly one user the acting user can reach — reachable meaning they share a Base channel, or the acting user is a Decile admin. A name shared by two reachable users is reported ambiguous rather than guessed. Unresolved tokens are left as literal text and notify nobody. The `mentions` object in the response reports each token as `resolved` or `unresolved`, the latter with a `reason` of `no_match`, `ambiguous`, or `not_permitted`.
      security:
      - api_key: []
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - post
              properties:
                post:
                  type: object
                  required:
                  - channel_id
                  - content
                  properties:
                    channel_id:
                      type: integer
                      description: Target Base channel ID
                    content:
                      type: string
                      description: Post body in Markdown
                    title:
                      type: string
                      description: Optional post title
      responses:
        '201':
          description: post created
          content:
            application/json:
              examples:
                success:
                  value:
                    success: true
                    post:
                      id: 555
                      title: Welcome to Base
                      created_at: '2026-04-29T15:30:00Z'
                      updated_at: '2026-04-29T15:30:00Z'
                      channel_id: 8
                      user_id: 7
                      user:
                        id: 7
                        first_name: John
                        last_name: Doe
                        email: john@example.com
                      content: Hello world
                      url: https://www.decilehub.com/base/8/posts/555
                      mentions:
                        resolved:
                        - resolved: true
                          token: "@Jane"
                          user_id: 12
                          name: Jane Roe
                        unresolved:
                        - resolved: false
                          token: "@Sam"
                          reason: ambiguous
        '400':
          description: missing required parameters
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                missing_params:
                  value:
                    error:
                      code: bad_request
                      message: 'Missing required parameters: channel_id, content'
                      field: channel_id
        '401':
          description: Unauthorized
        '403':
          description: Forbidden — API key lacks Base write access, or user cannot
            post to channel
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: channel not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                validation_failed:
                  value:
                    error:
                      code: validation_failed
                      message: Validation failed
                      details:
                      - Title can't be blank
  "/api/v1/base/posts/{id}":
    get:
      operationId: get_base_post
      x-agent-tool: true
      summary: Show a Decile Base post
      tags:
      - Base
      description: Returns a single Base post with its full thread of replies (ordered
        by creation time). Available to any API key with Base access; the acting user
        must also be authorized to view the post.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Base post ID
        schema:
          type: integer
      responses:
        '200':
          description: post returned
          content:
            application/json:
              examples:
                success:
                  value:
                    post:
                      id: 555
                      title: Welcome to Base
                      created_at: '2026-04-29T15:30:00Z'
                      updated_at: '2026-04-29T15:30:00Z'
                      channel_id: 8
                      user_id: 7
                      content: Hello world
                      url: https://www.decilehub.com/base/8/posts/555
                      user:
                        id: 7
                        first_name: John
                        last_name: Doe
                        email: john@example.com
                      replies:
                      - id: 9001
                        created_at: '2026-04-29T16:00:00Z'
                        updated_at: '2026-04-29T16:00:00Z'
                        base_post_id: 555
                        user_id: 8
                        parent_reply_id:
                        content: Thanks for the welcome!
                        user:
                          id: 8
                          first_name: Jane
                          last_name: Smith
                          email: jane@example.com
        '401':
          description: Unauthorized
        '403':
          description: Forbidden — API key lacks Base access, or user cannot view
            post
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: post not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/base/replies":
    post:
      operationId: create_base_reply
      x-agent-tool: true
      summary: Create a Decile Base reply
      tags:
      - Base
      description: |-
        Creates a reply to an existing Base post. The `content` is parsed as Markdown and stored as rich text. Available to any API key with Base access; the acting user must also be authorized to view the parent post. The reply is always attributed to the user who owns the API key; impersonation is not supported.

        `@FirstName` and `@FirstName LastName` tokens in `content` become real mentions, notifying the mentioned user. A token resolves only when it matches exactly one user the acting user can reach — reachable meaning they share a Base channel, or the acting user is a Decile admin. A name shared by two reachable users is reported ambiguous rather than guessed. Unresolved tokens are left as literal text and notify nobody. The `mentions` object in the response reports each token as `resolved` or `unresolved`, the latter with a `reason` of `no_match`, `ambiguous`, or `not_permitted`.
      security:
      - api_key: []
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - reply
              properties:
                reply:
                  type: object
                  required:
                  - base_post_id
                  - content
                  properties:
                    base_post_id:
                      type: integer
                      description: ID of the parent Base post
                    content:
                      type: string
                      description: Reply body in Markdown
      responses:
        '201':
          description: reply created
          content:
            application/json:
              examples:
                success:
                  value:
                    success: true
                    reply:
                      id: 9001
                      created_at: '2026-04-29T16:00:00Z'
                      updated_at: '2026-04-29T16:00:00Z'
                      base_post_id: 555
                      user_id: 8
                      content: Thanks for the welcome!
                      user:
                        id: 8
                        first_name: Jane
                        last_name: Smith
                        email: jane@example.com
                      url: https://www.decilehub.com/base/8/posts/555#reply_9001
                      mentions:
                        resolved:
                        - resolved: true
                          token: "@John Doe"
                          user_id: 7
                          name: John Doe
                        unresolved: []
        '400':
          description: missing required parameters
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                missing_params:
                  value:
                    error:
                      code: bad_request
                      message: 'Missing required parameters: base_post_id, content'
                      field: base_post_id
        '401':
          description: Unauthorized
        '403':
          description: Forbidden — API key lacks Base write access, or user cannot
            reply
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: parent post not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                validation_failed:
                  value:
                    error:
                      code: validation_failed
                      message: Validation failed
                      details:
                      - Content can't be blank
  "/api/v1/base/search":
    post:
      operationId: search_base
      x-agent-tool: true
      summary: Search Decile Base posts and articles
      tags:
      - Base
      description: Full-text search across Base posts (and Decile knowledge-base articles)
        via OpenSearch. Available to any API key with Base access. Searches the user's
        standard accessible channels and their child channels. Article results are
        suppressed when `channel_ids` is supplied.
      security:
      - api_key: []
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - search_term
              properties:
                search_term:
                  type: string
                  description: Free-text query
                channel_ids:
                  description: Optional list of channel IDs to restrict the search
                  oneOf:
                  - type: array
                    items:
                      type: integer
                  - type: string
                    description: Comma-separated channel IDs
                  example:
                  - 8
                  - 12
                channel_type:
                  type: string
                  description: Restrict to a single Base channel type
                  enum:
                  - question
                  - conversation
                authority:
                  type: string
                  description: Optional OpenSearch authority filter for article ranking
                article_limit:
                  type: integer
                  description: Maximum number of article results (default 5)
                  default: 5
                post_limit:
                  type: integer
                  description: Maximum number of post results (default 5)
                  default: 5
      responses:
        '200':
          description: search results returned
          content:
            application/json:
              examples:
                success:
                  value:
                    articles:
                    - id: 42
                      title: Getting started with Decile
                      url: https://help.decilehub.com/articles/42
                      score: 12.4
                    posts:
                    - id: 555
                      title: Welcome to Base
                      channel_id: 8
                      url: https://www.decilehub.com/base/8/posts/555
                      score: 9.1
        '400':
          description: missing search_term
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                missing_search_term:
                  value:
                    error:
                      code: bad_request
                      message: search_term is required
                      field: search_term
        '401':
          description: Unauthorized
        '403':
          description: Forbidden — API key lacks Base write access
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/base/attachments/{id}":
    get:
      operationId: download_base_attachment
      x-agent-tool: true
      summary: Download a Decile Base attachment
      tags:
      - Base
      description: Downloads an Active Storage blob attached to a Base post or reply.
        The `id` is the signed blob ID returned by other Base endpoints (see the `attachments`
        array on inbox items). Available to any API key with Base access; the acting
        user must additionally be authorized to view at least one parent Base record
        the blob is attached to. Files larger than 50 MB respond with a 302 redirect
        to a short-lived (5 minute) pre-signed storage URL; smaller files are streamed
        inline as `application/octet-stream` with a `Content-Disposition` of `attachment`.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Signed Active Storage blob ID
        schema:
          type: string
      responses:
        '200':
          description: file streamed inline (blobs ≤ 50 MB)
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '302':
          description: redirect to pre-signed download URL (blobs > 50 MB)
          headers:
            Location:
              description: Short-lived (5 minute) storage URL
              schema:
                type: string
                format: uri
        '401':
          description: Unauthorized
        '403':
          description: Forbidden — API key lacks Base access, or user cannot access
            blob
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: blob not found, signature invalid/expired, or file missing
            in storage
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                invalid_signature:
                  value:
                    error:
                      code: not_found
                      message: Invalid or expired attachment ID
                file_missing:
                  value:
                    error:
                      code: not_found
                      message: File not found in storage
  "/api/v1/email_templates":
    get:
      operationId: list_email_templates
      x-agent-tool: true
      summary: List email templates for the account
      tags:
      - Emails
      description: |
        Returns email templates visible to the authenticated account, ordered
        by id ascending and paginated 50 per page (0-indexed `page` param).
        Optionally filter by `pipeline_id` to restrict to templates authored
        on a specific pipeline. Use this to discover the `email_template_id`
        you'll pass to `POST /api/v1/emails/preview`,
        `POST /api/v1/emails`, or as a `send_email` action `data` override.

        Each list row carries enough to render a picker (name, pipeline,
        attachment count, admin/internal-only flags); use
        `GET /api/v1/email_templates/{id}` for the full subject/body and
        variable schema.
      security:
      - api_key: []
      parameters:
      - name: pipeline_id
        in: query
        description: Restrict to templates authored on this pipeline.
        schema:
          type: string
      - name: page
        in: query
        description: Page number (0-indexed). Page size is 50.
        schema:
          type: integer
          default: 0
      responses:
        '200':
          description: ok
          content:
            application/json:
              examples:
                success:
                  value:
                    data:
                    - id: '320'
                      name: Welcome
                      pipeline_id: '43'
                      pipeline_name: Investors
                      decile_admin_only_send: false
                      to_internal_actor_only: false
                      attachment_count: 1
                    - id: '321'
                      name: Follow-up
                      pipeline_id: '43'
                      pipeline_name: Investors
                      decile_admin_only_send: false
                      to_internal_actor_only: false
                      attachment_count: 0
                    pagination:
                      total_count: 2
                      current_page: 0
                      total_pages: 1
        '401':
          description: unauthorized (missing/invalid API key)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/email_templates/{id}":
    get:
      operationId: get_email_template
      x-agent-tool: true
      summary: Retrieve a single email template
      tags:
      - Emails
      description: |
        Returns the full template — index fields plus rendered (HTML)
        `rich_subject` / `body`, the resolved `variables` schema (each entry
        carries `name` and a `required` flag — `custom.*` variables are the
        ones callers must supply via `custom_substituted_variables` when
        previewing/sending), and any `attachments` (id, filename, byte
        size). Cross-account ids return 404.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Email template id.
        schema:
          type: string
      responses:
        '200':
          description: ok
          content:
            application/json:
              examples:
                success:
                  value:
                    id: '320'
                    name: Welcome
                    pipeline_id: '43'
                    pipeline_name: Investors
                    decile_admin_only_send: false
                    to_internal_actor_only: false
                    attachment_count: 1
                    rich_subject: "<div>Welcome to {{Pipeline.name}}</div>"
                    body: "<div>Hi {{Person.first_name}}, please supply {{custom.greeting}}.</div>"
                    variables:
                    - name: Person.first_name
                      required: false
                    - name: Pipeline.name
                      required: false
                    - name: custom.greeting
                      required: true
                    attachments:
                    - id: '88'
                      filename: welcome.pdf
                      byte_size: 42118
        '401':
          description: unauthorized (missing/invalid API key)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Template not found in the API key's account.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                not_found:
                  value:
                    error:
                      code: not_found
                      message: Email template not found
  "/api/v1/emails/preview":
    post:
      operationId: preview_email
      x-agent-tool: true
      summary: Preview a templated or freeform email before sending
      tags:
      - Emails
      description: |
        Resolves the target recipient, renders the chosen template (or
        freeform subject/body), substitutes variables, and reports
        unresolved variables / blocker warnings — without sending. Use this
        to surface the would-be email to a human (or agent) for
        confirmation before calling `POST /api/v1/emails`.

        **Target resolution: pass exactly one of**
        `pipeline_prospect_id`, `person_id`, `organization_id`, or
        `to_email`. Zero or multiple targets returns 422 (`missing_target`
        / `multiple_targets`). For `person_id` / `organization_id` /
        `to_email` targets, pipeline resolves in priority order:
        `email_template_id`'s pipeline → explicit `pipeline_id` →
        account default pipeline (connector → investor).
        Pass `pipeline_id` explicitly when the template is absent and
        you need the variable / actor scope to come from a specific
        pipeline. For `pipeline_prospect_id` the pipeline is derived
        from the prospect, and an explicit `pipeline_id` that doesn't
        match returns 422 `mismatched_pipeline`.

        **`cc` / `bcc`** — array of: raw email strings
        (`"alice@example.com"`), `{"person_id": "..."}` (account Person →
        email), or `{"internal_user_id": <numeric_id>}` (account user →
        email). Anything else returns 422 `invalid_recipient_format`.

        **Response envelope:** `recipient` (resolved person/email),
        `from` (the API key's user), `cc` / `bcc` (normalized email
        strings), `rendered` (subject + body html/text), `template`
        (id/name/pipeline/admin-only), `variables`
        (resolved / unresolved with reason), `attachments` (template +
        prospect-auto + signed-id uploads, tagged by `source`), `warnings`
        (blocker codes — `recipient_missing_email`,
        `decile_admin_only_template`, etc.), and `ready_to_send` (true
        only if no blockers and no unresolved variables).

        **Cross-account targets** (a `person_id` / `organization_id` /
        `pipeline_prospect_id` not visible to the API key's account) return
        404.
      security:
      - api_key: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                pipeline_prospect_id:
                  type: string
                  description: Target an existing pipeline prospect (pipeline derived
                    from prospect).
                person_id:
                  type: string
                  description: Target a Person; requires `pipeline_id`.
                organization_id:
                  type: string
                  description: Target an Organization; requires `pipeline_id`.
                to_email:
                  type: string
                  description: Target a raw email address; requires `pipeline_id`.
                pipeline_id:
                  type: string
                  description: Pipeline context for rendering. Required for non-prospect
                    targets.
                email_template_id:
                  type: string
                  description: Template to render. Omit to render freeform `rich_subject`
                    / `body`.
                rich_subject:
                  type: string
                  description: HTML subject override (also used freeform when no template).
                body:
                  type: string
                  description: HTML body override (also used freeform when no template).
                cc:
                  type: array
                  description: |
                    cc recipients. Each entry is one of: raw email string,
                    `{person_id: "..."}`, or `{internal_user_id: <id>}`.
                  items:
                    oneOf:
                    - type: string
                    - type: object
                      properties:
                        person_id:
                          type: string
                    - type: object
                      properties:
                        internal_user_id:
                          type: integer
                bcc:
                  type: array
                  description: Same shape as `cc`.
                  items:
                    oneOf:
                    - type: string
                    - type: object
                      properties:
                        person_id:
                          type: string
                    - type: object
                      properties:
                        internal_user_id:
                          type: integer
                attachments_attributes:
                  type: object
                  description: Mirror of the UI attachment-attributes form payload.
                  additionalProperties: true
                new_attachment_signed_ids:
                  type: array
                  description: ActiveStorage signed blob ids for new uploads to attach.
                  items:
                    type: string
                custom_substituted_variables:
                  type: object
                  description: Map of `custom.<name>` → value for unsupplied template
                    variables.
                  additionalProperties:
                    type: string
                email_from_actor:
                  type: string
                  description: From-actor URI override (e.g. `user:42`).
                scheduled_at:
                  type: string
                  format: date-time
                  description: ISO 8601 schedule time (informational at preview time).
            examples:
              prospect_target:
                summary: Preview a template against an existing prospect
                value:
                  pipeline_prospect_id: '183'
                  email_template_id: '320'
                  custom_substituted_variables:
                    custom.greeting: Hello
              person_target_with_cc:
                summary: Preview against a Person with a cc list
                value:
                  pipeline_id: 43nOmK6a
                  person_id: '1042'
                  email_template_id: '320'
                  cc:
                  - ops@example.com
                  - person_id: '1043'
      responses:
        '200':
          description: Preview envelope.
          content:
            application/json:
              examples:
                ok:
                  value:
                    recipient:
                      person_id: '5512'
                      name: Jane Investor
                      email: jane@example.com
                      missing_email: false
                    from:
                      actor_uri: user:42
                      display: Houston Searcy <houston@decilegroup.com>
                    cc:
                    - ops@example.com
                    bcc: []
                    rendered:
                      subject: Welcome to Acme Fund I
                      body_html: "<p>Hi Jane, thanks for joining.</p>"
                      body_text: Hi Jane, thanks for joining.
                    template:
                      id: '320'
                      name: Welcome
                      pipeline_id: 43nOmK6a
                      decile_admin_only: false
                    variables:
                      resolved:
                      - name: Person.first_name
                        value: Jane
                      unresolved: []
                    attachments:
                    - id: '88'
                      filename: welcome.pdf
                      source: template
                    warnings: []
                    ready_to_send: true
        '401':
          description: unauthorized (missing/invalid API key)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Target person / organization / prospect not in the API key's
            account.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: |
            Target resolution failed (`missing_target`, `multiple_targets`,
            `mismatched_pipeline`) or a cc/bcc entry was malformed
            (`invalid_recipient_format`).
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                missing_target:
                  value:
                    error:
                      code: missing_target
                      message: Specify one of pipeline_prospect_id, person_id, organization_id,
                        to_email.
                multiple_targets:
                  value:
                    error:
                      code: multiple_targets
                      message: 'Specify exactly one of: pipeline_prospect_id, person_id,
                        organization_id, to_email.'
                mismatched_pipeline:
                  value:
                    error:
                      code: mismatched_pipeline
                      message: pipeline_id does not match the pipeline of pipeline_prospect_id.
                invalid_recipient_format:
                  value:
                    error:
                      code: invalid_recipient_format
                      message: Recipient entry must be a string email, {person_id},
                        or {internal_user_id}.
  "/api/v1/emails":
    post:
      operationId: send_email
      x-agent-tool: true
      summary: Compose and send (or schedule, or test-send) an email
      tags:
      - Emails
      description: |
        Sends the previewed email. Request body is identical to
        `POST /api/v1/emails/preview` PLUS a required `confirm: true`
        acknowledgement, plus an optional `test_email_to_addresses` array
        for test-sends.

        **Two-step flow.** Always call `POST /api/v1/emails/preview` first
        and surface the envelope (especially `warnings`, `variables.unresolved`,
        `recipient.missing_email`) to a human or agent. Only call this
        endpoint once the preview's `ready_to_send` is true (or after
        supplying any missing `custom_substituted_variables`). Submitting
        without `confirm: true` returns 422 `confirmation_required` — by
        design, to prevent accidental sends.

        **Send modes** (`status` in response):
        - `sent` — default; dispatched immediately. `delivered_at` is the
          `PipelineEmail.created_at` ISO 8601.
        - `scheduled` — set when `scheduled_at` is supplied. `scheduled_at`
          in the response echoes the scheduled time.
        - `test_sent` — set when `test_email_to_addresses` is non-empty.
          The recipient is unchanged but the actual delivery goes to the
          test addresses; the prospect record is **not** persisted as a
          side effect (the synthesized-recipient persistence step is
          skipped). Use for sanity-checks during template authoring.

        **Server-side blockers** (before the send is attempted) surface
        as 422 with the matching preview-blocker code:
        `unresolved_variables`, `recipient_missing_email`,
        `decile_admin_only_template`. All preview-time errors
        (`missing_target`, `multiple_targets`, `mismatched_pipeline`,
        `invalid_recipient_format`) apply here too.
      security:
      - api_key: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - confirm
              properties:
                confirm:
                  type: boolean
                  description: |
                    Must be `true` (or the string `"true"`, `1`, `"1"`) to
                    acknowledge an intentional send. Missing/falsey →
                    422 `confirmation_required`.
                test_email_to_addresses:
                  type: array
                  description: |
                    If non-empty, send-as-test to these literal email
                    addresses instead of the resolved recipient.
                    Response `status` becomes `test_sent` and no prospect
                    record is persisted as a side effect.
                  items:
                    type: string
                pipeline_prospect_id:
                  type: string
                person_id:
                  type: string
                organization_id:
                  type: string
                to_email:
                  type: string
                pipeline_id:
                  type: string
                email_template_id:
                  type: string
                rich_subject:
                  type: string
                body:
                  type: string
                cc:
                  type: array
                  description: Same shape as `POST /api/v1/emails/preview` cc.
                  items:
                    oneOf:
                    - type: string
                    - type: object
                      properties:
                        person_id:
                          type: string
                    - type: object
                      properties:
                        internal_user_id:
                          type: integer
                bcc:
                  type: array
                  description: Same shape as `cc`.
                  items:
                    oneOf:
                    - type: string
                    - type: object
                      properties:
                        person_id:
                          type: string
                    - type: object
                      properties:
                        internal_user_id:
                          type: integer
                attachments_attributes:
                  type: object
                  additionalProperties: true
                new_attachment_signed_ids:
                  type: array
                  items:
                    type: string
                custom_substituted_variables:
                  type: object
                  additionalProperties:
                    type: string
                email_from_actor:
                  type: string
                scheduled_at:
                  type: string
                  format: date-time
            examples:
              send_to_prospect:
                summary: Send a template immediately to an existing prospect
                value:
                  pipeline_prospect_id: '183'
                  email_template_id: '320'
                  confirm: true
              schedule_to_person:
                summary: Schedule a template to a Person 1 hour from now
                value:
                  pipeline_id: 43nOmK6a
                  person_id: '1042'
                  email_template_id: '320'
                  scheduled_at: '2026-05-11T16:00:00Z'
                  confirm: true
              test_send:
                summary: Test-send to two literal addresses
                value:
                  pipeline_prospect_id: '183'
                  email_template_id: '320'
                  confirm: true
                  test_email_to_addresses:
                  - qa@example.com
                  - houston@decilegroup.com
      responses:
        '200':
          description: ok
          content:
            application/json:
              examples:
                sent:
                  summary: Sent immediately
                  value:
                    pipeline_email_id: '9921'
                    status: sent
                    recipient:
                      person_id: '5512'
                      name: Jane Investor
                      email: jane@example.com
                      missing_email: false
                    rendered:
                      subject: Welcome to Acme Fund I
                      body_html: "<p>Hi Jane, thanks for joining.</p>"
                      body_text: Hi Jane, thanks for joining.
                    delivered_at: '2026-05-11T15:01:22Z'
                    scheduled_at:
                scheduled:
                  summary: Scheduled for later
                  value:
                    pipeline_email_id: '9922'
                    status: scheduled
                    recipient:
                      person_id: '5512'
                      name: Jane Investor
                      email: jane@example.com
                      missing_email: false
                    rendered:
                      subject: Welcome to Acme Fund I
                      body_html: "<p>Hi Jane, thanks for joining.</p>"
                      body_text: Hi Jane, thanks for joining.
                    delivered_at:
                    scheduled_at: '2026-05-11T16:00:00Z'
                test_sent:
                  summary: Test-sent to literal addresses
                  value:
                    pipeline_email_id:
                    status: test_sent
                    recipient:
                      person_id: '5512'
                      name: Jane Investor
                      email: jane@example.com
                      missing_email: false
                    rendered:
                      subject: Welcome to Acme Fund I
                      body_html: "<p>Hi Jane, thanks for joining.</p>"
                      body_text: Hi Jane, thanks for joining.
                    delivered_at:
                    scheduled_at:
        '401':
          description: unauthorized (missing/invalid API key)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Target person / organization / prospect not in the API key's
            account.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: |
            `confirmation_required` (missing `confirm: true`),
            `unresolved_variables`, `recipient_missing_email`,
            `decile_admin_only_template`, plus all preview-time codes
            (`missing_target`, `multiple_targets`, `mismatched_pipeline`,
            `invalid_recipient_format`).
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                confirmation_required:
                  value:
                    error:
                      code: confirmation_required
                      message: 'Pass `confirm: true` to acknowledge that you intend
                        to send this email.'
                unresolved_variables:
                  value:
                    error:
                      code: unresolved_variables
                      message: Template has unresolved variables; supply them via
                        custom_substituted_variables and retry.
                recipient_missing_email:
                  value:
                    error:
                      code: recipient_missing_email
                      message: Recipient prospect has no email address.
                decile_admin_only_template:
                  value:
                    error:
                      code: decile_admin_only_template
                      message: This template can only be sent by Decile admins.
  "/api/v1/newsletters":
    get:
      operationId: list_newsletters
      x-agent-tool: true
      summary: List the account's newsletters
      tags:
      - Newsletters
      description: |
        Newest first, in the standard `{data, pagination}` envelope.
        `page` is 0-indexed; page size is 50.

        `status` is derived, never stored: `published` (`published_at`
        set), `scheduled` (a future `scheduled_at` with no
        `published_at`), otherwise `draft`.

        `body_mode` comes from the newsletter's companion email template
        and says which body will actually send: `html` (a full raw-HTML
        document, e.g. a builder export) or `rich_text` (the Trix body
        rendered inside Hub's newsletter layout). `filter` is the
        recipient-audience summary; `GET /api/v1/newsletters/{id}` returns
        the full filter with `recipient_count`.
      security:
      - api_key: []
      parameters:
      - name: page
        in: query
        required: false
        schema:
          type: integer
        description: 0-indexed page number.
      responses:
        '200':
          description: ok
          content:
            application/json:
              examples:
                ok:
                  value:
                    data:
                    - id: 9dK3mQx2
                      name: Q3 2026 LP Update
                      status: draft
                      body_mode: html
                      sending_method: default
                      scheduled_at:
                      published_at:
                      filter:
                        filter_group_type: pipeline
                        pipeline_id: 43nOmK6a
                        pipeline_name: Investors
                    pagination:
                      total_count: 1
                      current_page: 0
                      total_pages: 1
        '401':
          description: unauthorized (missing/invalid API key)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
    post:
      operationId: create_newsletter
      x-agent-tool: true
      summary: Create a draft newsletter with content and recipient filter
      tags:
      - Newsletters
      description: |
        Creates the newsletter, its companion pipeline and email template,
        and its recipient filter in one call. Nothing is mailed: the
        newsletter lands in `draft` and only
        `POST /api/v1/newsletters/{id}/publish` sends it.

        **Field mapping.** `subject` maps to the template's ActionText
        `rich_subject`; `body` maps to the ActionText `body`; `html_body`
        is the raw HTML column. Supply `html_body` for a complete HTML
        email document (mailed verbatim apart from the unsubscribe
        guarantee and the open pixel) or `body` for a rich-text fragment
        rendered inside Hub's newsletter layout. Passing `html_body`
        without `body_mode` sets `body_mode: html` automatically.

        **Recipient filter.** `filter_group_type` selects the audience
        mode: `pipeline` (needs `pipeline_id`; narrow further with
        `pipeline_stage_ids` / `tags`), `directory`, `manual` (needs
        `manual_recipient_person_ids`), `admitted_investors`, or
        `admitted_investors_fund` (needs
        `account_organization_fund_id`). Ids belonging to another account
        are rejected (`pipeline_id`, `account_organization_fund_id`) or
        dropped (`pipeline_stage_ids`, `manual_recipient_person_ids`).
        Unsubscribed addresses are excluded server-side on every send and
        cannot be re-included through this API.

        `html_body` is capped at 500KB by a model validation shared with
        the web editor; larger payloads return 422 `validation_failed`.
        Content or filter failures roll the whole creation back, so a
        rejected call leaves no partial newsletter behind.
      security:
      - api_key: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - name
              properties:
                name:
                  type: string
                  description: Internal newsletter name, unique per account.
                subject:
                  type: string
                  description: Email subject line (plain text; stored as `rich_subject`).
                body_mode:
                  type: string
                  enum:
                  - rich_text
                  - html
                  description: Which body sends. Defaults to `rich_text`, or `html`
                    when `html_body` is supplied.
                html_body:
                  type: string
                  description: Complete raw HTML email document (max 500KB).
                body:
                  type: string
                  description: Rich-text body HTML fragment.
                filter_group_type:
                  type: string
                  enum:
                  - pipeline
                  - directory
                  - manual
                  - admitted_investors
                  - admitted_investors_fund
                pipeline_id:
                  type: string
                  description: Source pipeline. Required when `filter_group_type`
                    is `pipeline`.
                pipeline_stage_ids:
                  type: array
                  description: Restrict pipeline recipients to these stage ids. Ids
                    not on the pipeline are dropped.
                  items:
                    type: string
                tags:
                  type: array
                  description: Restrict recipients to contacts carrying any of these
                    tags.
                  items:
                    type: string
                manual_recipient_person_ids:
                  type: array
                  description: Explicit Person ids to include. Ids outside the account
                    are dropped.
                  items:
                    type: string
                account_organization_fund_id:
                  type: string
                  description: Fund id. Required when `filter_group_type` is `admitted_investors_fund`.
                sending_method:
                  type: string
                  enum:
                  - default
                  - use_sendgrid_override
            examples:
              html_newsletter:
                summary: Paste a builder export and target a pipeline
                value:
                  name: Q3 2026 LP Update
                  subject: Q3 2026 at Acme Fund
                  body_mode: html
                  html_body: <html><body><h1>Hi {{prospectable.first_name}}</h1><p><a
                    href="{{unsubscribe_url}}">Unsubscribe</a></p></body></html>
                  filter_group_type: pipeline
                  pipeline_id: 43nOmK6a
              rich_text_newsletter:
                summary: Rich-text body to the whole directory
                value:
                  name: Welcome note
                  subject: Welcome
                  body: "<div>Thanks for joining.</div>"
                  filter_group_type: directory
      responses:
        '201':
          description: created
          content:
            application/json:
              examples:
                ok:
                  value:
                    data:
                      id: 9dK3mQx2
                      name: Q3 2026 LP Update
                      status: draft
                      body_mode: html
                      sending_method: default
                      scheduled_at:
                      published_at:
                      subject: Q3 2026 at Acme Fund
                      body: "<html><body><h1>Hi {{prospectable.first_name}}</h1></body></html>"
                      filter:
                        filter_group_type: pipeline
                        pipeline_id: 43nOmK6a
                        pipeline_name: Investors
                        pipeline_stage_ids: []
                        tags: []
                        manual_recipient_person_ids: []
                        account_organization_fund_id:
                        recipient_count: 128
                      delivery:
                        sent_count: 0
                        failed_count: 0
                        unsubscribed_count: 0
                        unique_open_count:
                        total_click_count:
                        unique_clicker_count:
                      rendering_note: body_html is author-supplied HTML rendered verbatim
                        in the email. Display it only in a sandboxed iframe (no allow-scripts);
                        never inject it into your own DOM.
        '401':
          description: unauthorized (missing/invalid API key)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: forbidden (readonly token, or caller is not a member of the
            account)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: |
            `validation_failed` (blank name, duplicate name, oversize
            `html_body`, unknown `body_mode`, cross-account
            `pipeline_id`).
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                oversize_html_body:
                  value:
                    error:
                      code: validation_failed
                      message: Validation failed
                      details:
                      - Html body is too long (maximum is 512000 characters)
  "/api/v1/newsletters/{id}":
    get:
      operationId: get_newsletter
      x-agent-tool: true
      summary: Get one newsletter with content, filter detail and delivery stats
      tags:
      - Newsletters
      description: |
        Adds subject, body content, the full recipient filter (with a
        freshly computed `recipient_count`) and delivery stats to the list
        payload.

        `body` is mode-dependent: the raw HTML document when `body_mode`
        is `html`, or the Trix body with its `trix-content` wrapper
        stripped when `body_mode` is `rich_text`. **It is author-supplied
        and unsanitized — render it only inside a sandboxed iframe, or
        display it as escaped text.**

        `delivery.sent_count` / `failed_count` come from the per-recipient
        `Newsletter::Send` rows. Open and click counts are `null` until
        the newsletter is published.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Newsletter id, as returned by `list_newsletters`.
        schema:
          type: string
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                type: object
        '401':
          description: unauthorized (missing/invalid API key)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Newsletter not found in the API key's account.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
    patch:
      operationId: update_newsletter
      x-agent-tool: true
      summary: Update a draft or scheduled newsletter
      tags:
      - Newsletters
      description: |
        Partial update: only the keys you send are touched. Content keys
        (`subject`, `body`, `html_body`, `body_mode`), settings keys
        (`name`, `sending_method`) and filter keys behave exactly as on
        create, including the same 500KB `html_body` cap and the same
        cross-account id rules.

        **A published newsletter is immutable** — returns 422
        `already_published`. Switching `body_mode` never destroys the
        other body; both stay in storage and the mode picks which one
        sends.

        Re-run `POST /api/v1/newsletters/{id}/preview` after any content
        change: the publish gate is evaluated from the stored content, not
        from what you sent here.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Newsletter id, as returned by `list_newsletters`.
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                subject:
                  type: string
                body_mode:
                  type: string
                  enum:
                  - rich_text
                  - html
                html_body:
                  type: string
                body:
                  type: string
                filter_group_type:
                  type: string
                  enum:
                  - pipeline
                  - directory
                  - manual
                  - admitted_investors
                  - admitted_investors_fund
                pipeline_id:
                  type: string
                pipeline_stage_ids:
                  type: array
                  items:
                    type: string
                tags:
                  type: array
                  items:
                    type: string
                manual_recipient_person_ids:
                  type: array
                  items:
                    type: string
                account_organization_fund_id:
                  type: string
                sending_method:
                  type: string
                  enum:
                  - default
                  - use_sendgrid_override
            examples:
              replace_html:
                summary: Replace the HTML document
                value:
                  html_body: "<html><body><p>Rewritten</p></body></html>"
      responses:
        '200':
          description: ok (same payload shape as GET)
          content:
            application/json:
              schema:
                type: object
        '401':
          description: unauthorized (missing/invalid API key)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: forbidden (readonly token, or caller is not a member of the
            account)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Newsletter not found in the API key's account.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: "`already_published`, or `validation_failed`."
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                already_published:
                  value:
                    error:
                      code: already_published
                      message: This newsletter has already been published and can
                        no longer be changed or re-sent.
  "/api/v1/newsletters/{id}/preview":
    post:
      operationId: preview_newsletter
      x-agent-tool: true
      summary: Render a newsletter and evaluate the publish gate without sending
      tags:
      - Newsletters
      description: |
        Renders the newsletter exactly as it will be mailed and returns
        the publish gate. Stateless: no row is written, no mail is sent.

        **Two-step flow.** Always call this before
        `POST /api/v1/newsletters/{id}/publish` and surface the rendered
        subject, the body, `recipient_count`, `warnings` and `blockers` to
        a human. A newsletter goes to the whole calculated audience at
        once and cannot be recalled.

        `rendered.body_html` is the finalized document: `{{unsubscribe_url}}`
        resolved, Hub's unsubscribe footer appended when the author placed
        no unsubscribe link, and the open-tracking pixel deliberately
        excluded. **It is author-supplied HTML — clients must render it
        only inside a sandboxed iframe with no script execution, never by
        injecting it into their own DOM.**

        `blockers` (publish refuses while non-empty): `blank_html_body`,
        `oversize_with_appended_footer` (finalized document past Gmail's
        ~102KB clip point while its only unsubscribe link is the
        auto-appended one, which the clip would remove).

        `warnings` (publish requires
        `acknowledge_unknown_variables: true`): `unknown_variables`,
        `oversize_document`, `foreign_merge_tags` (another ESP's merge
        syntax such as `*|FNAME|*` survived a paste and would ship
        literally). Each entry carries `code` and a `details` array.

        Rich-text newsletters return empty `warnings` and `blockers`;
        their existing send-time checks still apply.

        `recipient` is one representative recipient — the first calculated
        one unless you pass `person_id`. When the filter matches nobody,
        it falls back to the most recently added Person in the account so
        the render is still meaningful; `recipient_count` is `0` in that
        case and publishing would reach no one. `recipient_count` is
        otherwise the size of the real audience.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Newsletter id, as returned by `list_newsletters`.
        schema:
          type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                person_id:
                  type: string
                  description: Optional Person id to render against. Defaults to the
                    first calculated recipient, or the most recently added Person
                    in the account when the filter matches nobody.
      responses:
        '200':
          description: Preview envelope.
          content:
            application/json:
              examples:
                ok:
                  value:
                    data:
                      id: 9dK3mQx2
                      name: Q3 2026 LP Update
                      status: draft
                      body_mode: html
                      recipient:
                        person_id: '5512'
                        name: Jane Investor
                        email: jane@example.com
                        missing_email: false
                      recipient_count: 128
                      rendered:
                        subject: Q3 2026 at Acme Fund
                        body_html: <html><body><h1>Hi Jane</h1><p><a href="https://app.decilehub.com/unsubscribe/confirm?...">Unsubscribe</a></p></body></html>
                        body_text: Hi Jane Unsubscribe
                      variables:
                        resolved:
                        - name: prospectable.first_name
                          value: Jane
                        unresolved: []
                      warnings:
                      - code: foreign_merge_tags
                        details:
                        - "*|FNAME|*"
                      blockers: []
                      ready_to_publish: true
                      requires_acknowledgement: true
                      rendering_note: body_html is author-supplied HTML rendered verbatim
                        in the email. Display it only in a sandboxed iframe (no allow-scripts);
                        never inject it into your own DOM.
        '401':
          description: unauthorized (missing/invalid API key)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: forbidden (readonly token, or caller is not a member of the
            account)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Newsletter, or `person_id`, not found in the API key's account.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/newsletters/{id}/test":
    post:
      operationId: send_test_newsletter
      x-agent-tool: true
      summary: Send a test copy of the newsletter to literal addresses
      tags:
      - Newsletters
      description: |
        Delivers a test copy to `test_email_to_addresses` only. The
        calculated audience is never contacted, no per-recipient
        `Newsletter::Send` rows are created for them, and the newsletter's
        `status` is unchanged. Runs the same send path as the web editor's
        test-send button, so variables render against a representative
        recipient.

        This is a rehearsal, not the send: it does not replace the
        preview-before-publish step and does not take `confirm`.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Newsletter id, as returned by `list_newsletters`.
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - test_email_to_addresses
              properties:
                test_email_to_addresses:
                  type: array
                  description: Literal email addresses that receive the test copy.
                  items:
                    type: string
            examples:
              two_addresses:
                value:
                  test_email_to_addresses:
                  - qa@example.com
                  - houston@decilegroup.com
      responses:
        '200':
          description: ok
          content:
            application/json:
              examples:
                ok:
                  value:
                    data:
                      id: 9dK3mQx2
                      status: test_sent
                      test_email_to_addresses:
                      - qa@example.com
        '401':
          description: unauthorized (missing/invalid API key)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: forbidden (readonly token, or caller is not a member of the
            account)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Newsletter not found in the API key's account.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: |
            `invalid_parameter` (empty `test_email_to_addresses`),
            `no_recipients` (the filter matches nobody, so there is no
            recipient to render variables against), or `test_send_failed`
            with per-address details.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
  "/api/v1/newsletters/{id}/publish":
    post:
      operationId: publish_newsletter
      x-agent-tool: true
      summary: Publish (or schedule) a newsletter to its full audience
      tags:
      - Newsletters
      description: |
        Sends the newsletter to every calculated recipient, or schedules
        it. Irreversible: there is no unsend.

        **Requires `confirm: true`.** Missing or falsey returns 422
        `confirmation_required` — by design, to prevent accidental sends.

        **Preflight re-runs server-side**, so a stale preview cannot
        smuggle a broken newsletter through:
        - blockers present -> 422 `publish_blocked`, blocker entries in
          `error.details`.
        - warnings present without `acknowledge_unknown_variables: true`
          -> 422 `acknowledgement_required`, warning entries in
          `error.details`. Show them to the user and retry with the flag;
          it exists to record that a human saw them, so do not set it
          pre-emptively on the first attempt.

        **Scheduling.** A future ISO 8601 `scheduled_at` schedules instead
        of sending and stamps the caller as the sender; a past timestamp
        returns 422 `invalid_parameter`. Omit it to send immediately.

        Unsubscribed addresses are excluded inside recipient calculation
        and cannot be bypassed. Republishing is not exposed: a published
        newsletter returns 422 `already_published`.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Newsletter id, as returned by `list_newsletters`.
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - confirm
              properties:
                confirm:
                  type: boolean
                  description: Must be `true` (or `"true"`, `1`, `"1"`).
                acknowledge_unknown_variables:
                  type: boolean
                  description: Acknowledges every preflight warning. Set only after
                    showing them to the user.
                scheduled_at:
                  type: string
                  format: date-time
                  description: Future ISO 8601 send time. When set, the newsletter
                    is scheduled instead of sent now.
            examples:
              send_now:
                value:
                  confirm: true
              acknowledge_and_send:
                value:
                  confirm: true
                  acknowledge_unknown_variables: true
              schedule:
                value:
                  confirm: true
                  scheduled_at: '2026-08-11T16:00:00Z'
      responses:
        '200':
          description: ok
          content:
            application/json:
              examples:
                published:
                  value:
                    data:
                      id: 9dK3mQx2
                      status: published
                      recipient_count: 128
                      published_at: '2026-08-04T15:01:22Z'
                      scheduled_at:
                scheduled:
                  value:
                    data:
                      id: 9dK3mQx2
                      status: scheduled
                      recipient_count: 128
                      published_at:
                      scheduled_at: '2026-08-11T16:00:00Z'
        '401':
          description: unauthorized (missing/invalid API key)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '403':
          description: forbidden (readonly token, or caller is not a member of the
            account)
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '404':
          description: Newsletter not found in the API key's account.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
        '422':
          description: |
            `confirmation_required`, `publish_blocked`,
            `acknowledgement_required`, `invalid_parameter` (bad or past
            `scheduled_at`), or `already_published`.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              examples:
                confirmation_required:
                  value:
                    error:
                      code: confirmation_required
                      message: 'Pass `confirm: true` to acknowledge that you intend
                        to send this newsletter to every calculated recipient.'
                publish_blocked:
                  value:
                    error:
                      code: publish_blocked
                      message: 'Preflight blocked this newsletter: blank_html_body.
                        Fix the content and re-run preview_newsletter.'
                      details:
                      - code: blank_html_body
                        details: []
                acknowledgement_required:
                  value:
                    error:
                      code: acknowledgement_required
                      message: 'Preflight raised warnings: unknown_variables. Show
                        them to the user, then retry with `acknowledge_unknown_variables:
                        true`.'
                      details:
                      - code: unknown_variables
                        details:
                        - not_a_real_variable
  "/api/v1/variables":
    get:
      summary: List firm variables
      tags:
      - Variables
      description: |
        Returns the variable picker for a pipeline + the authenticated
        user. Mirrors exactly what the user sees in the email composer's
        variable list — same per-user policy filtering.

        The `display_format` field is the merge-tag form (e.g.
        `email.fund_thesis`, `sender.name`) that gets wrapped in `{{...}}`
        inside an email template body. `select_options` is only present
        when `format_name == "Select (Dropdown)"`. The `id` is opaque —
        treat it as a string and pass it back to
        `GET /api/v1/variables/{id}` to look up a single row.
      security:
      - api_key: []
      parameters:
      - name: pipeline_id
        in: query
        description: Pipeline id (obscure or numeric). Required — variables are scoped
          per pipeline and per user.
        required: true
        schema:
          type: string
      - name: page
        in: query
        description: 0-indexed page number. Page size is 50.
        required: false
        schema:
          type: integer
      responses:
        '200':
          description: Variables found
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/variable"
                  pagination:
                    type: object
                    properties:
                      total_count:
                        type: integer
                      current_page:
                        type: integer
                      total_pages:
                        type: integer
        '400':
          description: Bad request (pipeline_id missing)
        '401':
          description: Unauthorized
        '404':
          description: Pipeline not found
  "/api/v1/variables/{id}":
    get:
      summary: Show a single variable
      tags:
      - Variables
      description: |
        Returns a single variable from the pipeline's picker for the
        authenticated user. The `id` path parameter is the value returned
        by `GET /api/v1/variables`; treat it as opaque. Unknown ids,
        variables not surfaced in the user's picker, and variables not
        accessible to the authenticated account return 404.
      security:
      - api_key: []
      parameters:
      - name: id
        in: path
        required: true
        description: Variable id as returned by the list endpoint.
        schema:
          type: string
      - name: pipeline_id
        in: query
        required: true
        description: Pipeline id (obscure or numeric). Required — variables are scoped
          per pipeline and per user.
        schema:
          type: string
      responses:
        '200':
          description: Variable found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/variable"
        '400':
          description: Bad request (pipeline_id missing)
        '401':
          description: Unauthorized
        '404':
          description: Not found
  "/api/v1/agent_platform/file_search":
    post:
      tags:
      - agent_platform
      summary: Semantic search across the caller's account corpus
      description: |
        Returns ranked chunks from the caller account's data room + any
        in-message conversation-scoped chunks the caller can read.
        Optional conversation_id filter restricts results to that
        conversation (caller must have access to it).
      security:
      - api_key: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - q
              properties:
                q:
                  type: string
                  maxLength: 2000
                conversation_id:
                  type: integer
                attachment_ids:
                  type: array
                  items:
                    type: integer
                limit:
                  type: integer
                  minimum: 1
                  maximum: 50
                  default: 25
      responses:
        '200':
          description: Matches array
        '400':
          description: Input validation failed
        '401':
          description: Missing or invalid API key
        '403':
          description: Caller lacks access to the requested conversation_id filter
servers:
- url: "{protocol}://{host}"
  variables:
    protocol:
      default: https
    host:
      default: www.decilehub.com
components:
  securitySchemes:
    api_key:
      type: apiKey
      name: Authorization
      in: header
  schemas:
    TaskDecisionRequest:
      type: object
      required:
      - decision
      - confirm
      properties:
        decision:
          type: string
          enum:
          - approve
          - decline
        notes:
          type: string
          description: Optional evidence or context; retries must match the normalized
            recorded notes.
        confirm:
          type: boolean
          description: Must be true for either decision.
    TaskDecisionResponse:
      type: object
      required:
      - data
      - decision
      - execution_status
      properties:
        data:
          "$ref": "#/components/schemas/Task"
        decision:
          type: string
          enum:
          - approve
          - decline
        execution_status:
          type: string
          enum:
          - decided
          - already_decided
    attached_image:
      type: object
      nullable: true
      description: 'An attached image (Person picture / Organization logo). Null when
        no image is attached. `url` is a signed ActiveStorage download link to the
        original file.

        '
      properties:
        url:
          type: string
          description: Absolute, signed download URL for the original image.
        content_type:
          type: string
          example: image/png
        byte_size:
          type: integer
        filename:
          type: string
    PortfolioCompany:
      type: object
      description: A fund's stake in a single underlying organization.
      properties:
        id:
          type: integer
        name:
          type: string
          description: Display name of the underlying organization.
        organization_id:
          type: integer
        firm_admin_fund_details_id:
          type: integer
          description: FK to firm_admin_fund_details.id — the fund holding this company.
        entity_type:
          type: string
          nullable: true
        initial_investment_date:
          type: string
          format: date
          nullable: true
        investment_thesis:
          type: string
          nullable: true
        reserve_consumed:
          type: string
          nullable: true
          description: Decimal as string. USD already deployed from the fund's reserves.
        board_representation:
          type: object
          description: JSONB bag of board seat details. Keys may include board_seats,
            observer_seats, notes.
        exit_strategy:
          type: object
          description: JSONB bag of free-form exit strategy notes. Keys may include
            notes, target_exit_year, exit_type.
        legal_name:
          type: string
          nullable: true
          description: Legal entity name.
        display_legal_name:
          type: string
          nullable: true
          description: Legal name suitable for display.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    PortfolioCompanyInvestment:
      type: object
      description: One investment tranche on a portfolio company, driven by an underlying
        accounting transaction.
      properties:
        id:
          type: integer
        firm_admin_portfolio_companies_id:
          type: integer
          description: FK to firm_admin_portfolio_companies.id — the parent portfolio
            company.
        firm_admin_accounting_transactions_id:
          type: integer
          nullable: true
          description: FK to firm_admin_accounting_transactions.id — the source transaction
            that created this investment.
        shares:
          type: integer
          nullable: true
          description: Integer share count. Legacy column; prefer shares_decimal for
            calculations.
        shares_decimal:
          type: string
          nullable: true
          description: Decimal share count as string (numeric 20,8). Primary share
            column since 2025-09-08.
        ignore_for_fmv_calc:
          type: boolean
          description: TRUE = exclude this tranche from FMV calculations.
        current_fmv:
          type: string
          description: Normal-mode expanded UI tranche fair market value in USD as
            a decimal string.
        current_mark_price_per_share:
          type: string
          description: Normal-mode expanded UI tranche mark price per share as a decimal
            string.
        security_type:
          type: string
          nullable: true
          description: Effective security type from the linked funding round as of
            the selected date.
        security_series:
          type: string
          nullable: true
          description: Series or unit class from the linked funding round.
        company_funding_round_id:
          type: integer
          nullable: true
          description: Linked funding round id.
        valuation_event_id:
          type: integer
          nullable: true
          description: Valuation event used for the current mark, when applicable.
        fmv_source_type:
          type: string
          enum:
          - valuation_event
          - tranche_override
          - cost_basis
          description: Source category used for the current mark.
        fmv_as_of_date:
          type: string
          format: date
          nullable: true
          description: Effective date of the current mark source.
        valuation_basis:
          type: string
          enum:
          - price_per_share
          - fmv_override
          - cost_basis
          - full_exit
          - partial_exit
          - excluded_from_fmv
          - markdown
          - linked_funding_round_price_per_share
          description: Calculation basis used for the current mark.
        tax_born_on:
          type: string
          format: date
          nullable: true
          description: Date used for tax holding period start. Defaults to transaction_at
            when NULL.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    PortfolioCompanyValuationSummary:
      type: object
      description: One valuation event in a portfolio company's history.
      properties:
        id:
          type: integer
        valuation_date:
          type: string
          format: date
        valuation_type:
          type: string
          enum:
          - investment
          - mark_up
          - mark_down
          - full_exit
          - partial_exit
        reason:
          type: string
          enum:
          - funding_round
          - business_performance
          - market_development
          - strategic_partnership
          - key_personnel_change
          - regulatory_change
          - other_reason
          - with_full_cost_recovery
          - with_gain
          - with_loss
          - return_of_capital
          - market_price_update
        mark_amount:
          type: string
          nullable: true
          description: Total mark value as a decimal string.
        price_per_share:
          type: string
          nullable: true
          description: Price per share as a decimal string.
        shares_outstanding:
          type: string
          nullable: true
          description: Share count as a string.
        approved:
          type: boolean
        audited:
          type: boolean
        company_funding_round_id:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ScheduleOfInvestmentsMetadata:
      type: object
      required:
      - entity_id
      - entity_kind
      - entity_name
      - as_of_date
      - valuation_basis
      - company_count
      - position_count
      properties:
        entity_id:
          type: integer
        entity_kind:
          type: string
          enum:
          - fund
          - spv
          - holding
        entity_name:
          type: string
        as_of_date:
          type: string
          format: date
        valuation_basis:
          type: string
          enum:
          - gaap
        company_count:
          type: integer
        position_count:
          type: integer
    ScheduleOfInvestmentsCompany:
      type: object
      required:
      - company_id
      - name
      - positions
      - subtotal
      properties:
        company_id:
          type: integer
        name:
          type: string
        positions:
          type: array
          items:
            "$ref": "#/components/schemas/ScheduleOfInvestmentsPosition"
        subtotal:
          "$ref": "#/components/schemas/ScheduleOfInvestmentsTotals"
    ScheduleOfInvestmentsPosition:
      type: object
      description: One canonical SOI investment position or tranche.
      properties:
        position_id:
          type: integer
        company_id:
          type: integer
        transaction_date:
          type: string
          format: date
          nullable: true
        effective_date:
          type: string
          format: date
        tax_born_on_date:
          type: string
          format: date
          nullable: true
        maturity_date:
          type: string
          format: date
          nullable: true
        funding_round_id:
          type: integer
          nullable: true
        funding_round_name:
          type: string
          nullable: true
        security_name:
          type: string
          nullable: true
        security_type:
          type: string
          enum:
          - equity
          - safe
          - note
          - warrant
          - token
          - revenue_share
          - saft
          - partnership_interest
        security_series:
          type: string
          nullable: true
        shares:
          type: string
          nullable: true
        cost_basis:
          type: string
        current_fmv:
          type: string
        cost_per_share:
          type: string
          nullable: true
        mark_price_per_share:
          type: string
          nullable: true
        unrealized_gain_loss:
          type: string
        excluded_from_totals:
          type: boolean
        valuation_event:
          "$ref": "#/components/schemas/ScheduleOfInvestmentsValuationEvent"
    ScheduleOfInvestmentsValuationEvent:
      type: object
      properties:
        id:
          type: integer
          nullable: true
        date:
          type: string
          format: date
          nullable: true
        source:
          type: string
          nullable: true
          description: Supporting-documentation source, or `cost_basis` for a synthetic
            carry-at-cost mark.
        synthetic:
          type: boolean
        funding_round_id:
          type: integer
          nullable: true
        funding_round_name:
          type: string
          nullable: true
    ScheduleOfInvestmentsTotals:
      type: object
      required:
      - shares
      - cost_basis
      - current_fmv
      - unrealized_gain_loss
      properties:
        shares:
          type: string
        cost_basis:
          type: string
        current_fmv:
          type: string
        unrealized_gain_loss:
          type: string
    PortfolioCompanyValuationAttachmentReference:
      type: object
      description: An authorized document attached directly to the valuation or one
        of its company's funding rounds.
      properties:
        id:
          type: integer
        name:
          type: string
      required:
      - id
      - name
    PortfolioCompanyValuationUserReference:
      type: object
      nullable: true
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email
    PortfolioCompanyValuationFundingRound:
      type: object
      description: Authorized terms and document references for the round linked to
        a valuation event.
      properties:
        id:
          type: integer
        round:
          type: string
          nullable: true
        close_date:
          type: string
          format: date
          nullable: true
        security:
          type: string
          nullable: true
        unit_class:
          type: string
          nullable: true
          description: Security series or unit/share class, such as Series A Preferred.
        price_per_share:
          type: string
          nullable: true
        amount_raised:
          type: string
          nullable: true
        currency:
          type: string
          nullable: true
        pre_money_valuation:
          type: string
          nullable: true
        post_money_valuation:
          type: string
          nullable: true
        valuation_cap:
          type: string
          nullable: true
        valuation_type:
          type: string
          nullable: true
        converted_to:
          type: string
          nullable: true
        converted_when:
          type: string
          format: date-time
          nullable: true
        discount:
          type: string
          nullable: true
        cap_adjusted_discount:
          type: string
          nullable: true
        cap_adjusted_price_per_share:
          type: string
          nullable: true
        cap_adjusted_valuation_cap:
          type: string
          nullable: true
        approved:
          type: boolean
        open:
          type: boolean
        attachments:
          type: array
          items:
            "$ref": "#/components/schemas/PortfolioCompanyValuationAttachmentReference"
    PortfolioCompanyValuationDetail:
      type: object
      description: Complete valuation-event provenance, with an optional all-funding-rounds
        expansion.
      properties:
        id:
          type: integer
        portfolio_company_id:
          type: integer
        organization_id:
          type: integer
        valuation_date:
          type: string
          format: date
        valuation_type:
          type: string
        reason:
          type: string
        notes:
          type: string
          nullable: true
        mark_amount:
          type: string
          nullable: true
        exit_amount:
          type: string
          nullable: true
        price_per_share:
          type: string
          nullable: true
        shares_outstanding:
          type: string
          nullable: true
        valuation_cap:
          type: string
          nullable: true
        repurchase_shares:
          type: string
          nullable: true
        repurchase_pps:
          type: string
          nullable: true
        override_fmv_share_price_with_cost_basis_share_price:
          type: boolean
        cap_adjusted_discount:
          type: string
          nullable: true
        cap_adjusted_price_per_share:
          type: string
          nullable: true
        cap_adjusted_valuation_cap:
          type: string
          nullable: true
        approved:
          type: boolean
        audited:
          type: boolean
        audited_at:
          type: string
          format: date-time
          nullable: true
        creator:
          "$ref": "#/components/schemas/PortfolioCompanyValuationUserReference"
        auditor:
          "$ref": "#/components/schemas/PortfolioCompanyValuationUserReference"
        company_funding_round_id:
          type: integer
          nullable: true
        attachments:
          type: array
          items:
            "$ref": "#/components/schemas/PortfolioCompanyValuationAttachmentReference"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        funding_rounds:
          type: array
          description: All kept, authorized funding rounds for the valuation company,
            newest first.
          items:
            "$ref": "#/components/schemas/PortfolioCompanyValuationFundingRound"
    PortfolioCompanyInvestmentDetail:
      type: object
      description: |
        A single investment tranche on a portfolio company with applied overrides resolved.
        Combines the `PortfolioCompanyInvestment` column allowlist with computed/resolved
        fields. Sparse `fields=` can address either group.
      properties:
        id:
          type: integer
        firm_admin_portfolio_companies_id:
          type: integer
          description: FK to firm_admin_portfolio_companies.id — the parent portfolio
            company.
        firm_admin_accounting_transactions_id:
          type: integer
          nullable: true
          description: FK to firm_admin_accounting_transactions.id — the source transaction
            that created this investment.
        shares:
          type: integer
          nullable: true
          description: Integer share count. Legacy column; prefer shares_decimal for
            calculations.
        shares_decimal:
          type: string
          nullable: true
          description: Decimal share count as string (numeric 20,8). Primary share
            column since 2025-09-08.
        ignore_for_fmv_calc:
          type: boolean
          description: TRUE = exclude this tranche from FMV calculations.
        tax_born_on:
          type: string
          format: date
          nullable: true
          description: Date used for tax holding period start. Defaults to transaction_at
            when NULL.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        effective_shares:
          type: string
          description: Resolved share count (shares_decimal if present, else shares,
            else 0).
        effective_tax_born_date:
          type: string
          format: date
          nullable: true
          description: Resolved tax-holding-period start (tax_born_on if present,
            else transaction_at::date).
        price_per_share:
          type: string
          description: Resolved per-share price. Uses legal_cost_per_share when set;
            otherwise transaction_amount / effective_shares.
        transaction_amount:
          type: string
          description: Resolved cost basis (USD). Applies write-off (returns -write_off_investment_new_cost)
            and convertible-debt conversion logic when applicable; otherwise sourced
            from the underlying accounting transaction.
        transaction_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp of the underlying accounting transaction.
        current_fmv:
          type: string
          description: Normal-mode expanded UI tranche fair market value in USD as
            a decimal string.
        current_mark_price_per_share:
          type: string
          description: Normal-mode expanded UI tranche mark price per share as a decimal
            string.
        security_type:
          type: string
          nullable: true
          description: Effective security type from the linked funding round as of
            the selected date.
        security_series:
          type: string
          nullable: true
          description: Series or unit class from the linked funding round.
        company_funding_round_id:
          type: integer
          nullable: true
          description: Linked funding round id.
        valuation_event_id:
          type: integer
          nullable: true
          description: Valuation event used for the current mark, when applicable.
        fmv_source_type:
          type: string
          enum:
          - valuation_event
          - tranche_override
          - cost_basis
        fmv_as_of_date:
          type: string
          format: date
          nullable: true
        valuation_basis:
          type: string
          enum:
          - price_per_share
          - fmv_override
          - cost_basis
          - full_exit
          - partial_exit
          - excluded_from_fmv
          - markdown
          - linked_funding_round_price_per_share
        mark_provenance:
          type: object
          description: Concise link to the exact source record used for the mark.
          properties:
            source_type:
              type: string
              enum:
              - valuation_event
              - tranche_override
              - cost_basis
            source_record_id:
              type: integer
              nullable: true
              description: ID of the valuation event, tranche FMV override, markdown
                organization, ignored investment, or ordinary cost-basis accounting
                transaction used as the source.
            effective_date:
              type: string
              format: date
              nullable: true
            valuation_basis:
              type: string
            valuation_event_id:
              type: integer
              nullable: true
            company_funding_round_id:
              type: integer
              nullable: true
            contributing_sources:
              type: array
              description: Present only for composite marks. Includes the base mark
                source followed by every applied partial-exit valuation event in effective-date
                order.
              items:
                type: object
                properties:
                  source_type:
                    type: string
                    enum:
                    - valuation_event
                    - tranche_override
                    - cost_basis
                  source_record_id:
                    type: integer
                    nullable: true
                  effective_date:
                    type: string
                    format: date
                    nullable: true
                  valuation_basis:
                    type: string
                  valuation_event_id:
                    type: integer
                    nullable: true
                  company_funding_round_id:
                    type: integer
                    nullable: true
    ActivityEntry:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
          nullable: true
          description: Cached display name of the subject at the time of the event.
        subject_type:
          type: string
          nullable: true
        subject_id:
          type: integer
          nullable: true
        entryable_type:
          type: string
          nullable: true
          description: Fully qualified Log::ActivityEntries::* class name.
        user_id:
          type: integer
          nullable: true
          description: Actor user id; null for automated/system events.
        account_id:
          type: integer
        origin:
          type: string
          nullable: true
          description: Per-request origin tag (api, mcp, internal_service, chrome_extension);
            null = web/UI/system.
        created_at:
          type: string
          format: date-time
        entryable:
          type: object
          nullable: true
          description: Present only when include=entryable is requested. Minimal type-tagged
            shape.
          properties:
            type:
              type: string
              description: Short entryable class name (e.g. Note, Audit, Gmail).
            action:
              type: string
              nullable: true
    ActivityEntryDetail:
      type: object
      description: Single activity entry returned by GET /api/v1/activity_entries/{id},
        including the full entryable body.
      properties:
        id:
          type: integer
        name:
          type: string
          nullable: true
        subject_type:
          type: string
          nullable: true
        subject_id:
          type: integer
          nullable: true
        entryable_type:
          type: string
          nullable: true
        entryable_id:
          type: integer
          nullable: true
        user_id:
          type: integer
          nullable: true
        account_id:
          type: integer
        origin:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        entryable:
          type: object
          nullable: true
          description: |
            Full type-specific body. Always carries a `type` key (short entryable
            class name); remaining keys vary by type.
          properties:
            type:
              type: string
              description: Short entryable class name (e.g. Note, Audit, Gmail, AmlKyc).
            action:
              type: string
              nullable: true
              description: Present on Audit, CapitalAccount, Gmail, EmailDispatch,
                LpMatching, CapTable.
            item_type:
              type: string
              nullable: true
              description: Audit — class name of the changed record.
            item_id:
              type: integer
              nullable: true
              description: Audit — id of the changed record.
            changes:
              type: object
              nullable: true
              description: 'Audit — changed-attribute diff `{ "col": [old, new] }`,
                with redacted columns removed.'
            subject:
              type: string
              nullable: true
              description: Gmail / EmailDispatch — email subject line.
            from:
              type: string
              nullable: true
              description: Gmail / EmailDispatch — sender address.
            to:
              type: string
              nullable: true
              description: Gmail — recipient address(es).
            email_template:
              type: string
              nullable: true
              description: EmailDispatch — template name, or "Custom (without template)".
            body:
              type: string
              nullable: true
              description: Note body, or full Gmail / EmailDispatch email body.
            delivered_at:
              type: string
              format: date-time
              nullable: true
              description: EmailDispatch — first delivery event timestamp.
            opened_at:
              type: string
              format: date-time
              nullable: true
              description: EmailDispatch — first open event timestamp.
            recipients:
              type: object
              nullable: true
              description: EmailDispatch — resolved to/cc/bcc recipient lines.
            links_clicked:
              type: object
              nullable: true
              description: EmailDispatch — clicked URLs mapped to click counts.
            attachments:
              type: array
              nullable: true
              items:
                type: string
              description: EmailDispatch — attachment filenames.
            description:
              type: string
              nullable: true
              description: CompletedTask description.
            data:
              type: object
              nullable: true
              description: Merge and PipelineActivity / CapTable payload.
            aml_status:
              type: string
              nullable: true
            kyc_status:
              type: string
              nullable: true
            summary:
              type: string
              nullable: true
              description: ChatSummary text.
    DealShare:
      type: object
      description: A deal share submitted by the caller's account into the network
        feed.
      properties:
        id:
          type: integer
        organization_id:
          type: integer
          nullable: true
        company_name:
          type: string
        the_bet:
          type: string
        referring_manager_name:
          type: string
        referring_manager_email:
          type: string
        founder:
          type: string
          nullable: true
        short_description:
          type: string
          nullable: true
        region:
          type: string
          nullable: true
        country_address:
          type: string
          nullable: true
        company_email:
          type: string
          nullable: true
        deck_url:
          type: string
          nullable: true
        round:
          type: string
          nullable: true
        anticipated_close_date:
          type: string
          format: date
          nullable: true
        why:
          type: string
          nullable: true
        investor_role:
          type: string
          nullable: true
        deal_source:
          type: string
          nullable: true
        traction_metrics:
          type: string
          nullable: true
        amount_raising:
          type: number
          nullable: true
        deal_terms:
          type: string
          nullable: true
        valuation:
          type: number
          nullable: true
        industry_list:
          type: array
          items:
            type: string
        stage:
          type: string
          description: Computed stage (e.g. "Unfiltered New Deals", "Expiring Deals",
            "Expired Deals").
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    DealShareList:
      type: object
      properties:
        data:
          type: array
          items:
            "$ref": "#/components/schemas/DealShare"
    DealShareDetail:
      type: object
      properties:
        data:
          "$ref": "#/components/schemas/DealShare"
    PipelineMetricsSnapshot:
      type: object
      properties:
        total_prospects:
          type: integer
        by_stage:
          type: object
          additionalProperties:
            type: integer
        committed_amount:
          type: integer
        weighted_pipeline:
          type: integer
        as_of:
          type: string
          format: date-time
    note_envelope:
      type: object
      properties:
        data:
          type: object
          properties:
            id:
              type: integer
            body:
              type: string
            context:
              type: string
              nullable: true
            created_at:
              type: string
              format: date-time
            author:
              type: object
              properties:
                id:
                  type: integer
                first_name:
                  type: string
                last_name:
                  type: string
    ErrorResponse:
      type: object
      required:
      - error
      properties:
        error:
          type: object
          required:
          - code
          - message
          properties:
            code:
              type: string
              example: invalid_timezone
            message:
              type: string
              example: Invalid timezone 'Invalid/Timezone'
            field:
              type: string
              nullable: true
              example: event_when_timezone
            valid_values:
              type: array
              nullable: true
              items:
                type: string
              example:
              - America/New_York
              - Etc/UTC
            details:
              nullable: true
              oneOf:
              - type: array
                items:
                  oneOf:
                  - type: string
                  - type: object
              - type: object
              example:
              - Event title can't be blank
    Entity:
      type: object
      description: Identity block returned by `GET /api/v1/entities` (list) and as
        the outer object on `GET /api/v1/entities/{id}` (show). Show responses also
        include a `details` key (see `EntityDetails`).
      required:
      - id
      - name
      - kind
      - is_active
      properties:
        id:
          type: integer
        name:
          type: string
          description: Display name (fund_details.entity_name when present, else organization
            name, else account name).
        kind:
          type: string
          enum:
          - fund
          - spv
          - holding
          - management_company
          - general_partnership
          - unknown
        currency:
          type: string
          nullable: true
          example: USD
        fiscal_year_end:
          type: string
          nullable: true
          description: "`MM-DD` (e.g. `12-31`) for fund-kind entities with a configured
            fiscal_year_end_month; null otherwise."
          example: 12-31
        is_active:
          type: boolean
          description: "`true` when the entity is active / not hidden (`hidden_from_left_nav=false`)."
    EntityDetails:
      type: object
      description: Detail block on `GET /api/v1/entities/{id}`. `gp_carry`, `new_management_fee_system_cutover_date`,
        and `new_carry_system_cutover_date` are only present when the entity is a
        fund (kind=`fund`, `spv`, or `holding`).
      required:
      - currency
      - currency_symbol
      - fiscal_year_end
      - entity_name
      - logo_url
      - is_active
      - kind
      properties:
        currency:
          type: string
          nullable: true
        currency_symbol:
          type: string
          nullable: true
        fiscal_year_end:
          type: string
          nullable: true
          example: 12-31
        entity_name:
          type: string
        logo_url:
          type: string
          nullable: true
        is_active:
          type: boolean
        kind:
          type: string
          enum:
          - fund
          - spv
          - holding
          - management_company
          - general_partnership
          - unknown
        gp_carry:
          type: number
          format: double
          nullable: true
          description: Fund-only. Float on a 0–1 scale (e.g. `0.20` means 20%); null
            when unset.
        new_management_fee_system_cutover_date:
          type: string
          format: date
          nullable: true
          description: Fund-only.
        new_carry_system_cutover_date:
          type: string
          format: date
          nullable: true
          description: Fund-only.
        thesis:
          type: string
          nullable: true
          description: Fund-only. Free-text investment thesis.
        target_fund_size:
          type: integer
          nullable: true
          description: Fund-only. Whole-dollar target raise; 0 when unset.
        description:
          type: string
          nullable: true
          description: Fund-only. Public-facing fund description.
        news:
          type: string
          nullable: true
          description: Fund-only. LP-facing news/announcements.
        deck_url:
          type: string
          nullable: true
          description: Fund-only. URL to the fund pitch deck.
        cap_table_link:
          type: string
          nullable: true
          description: Fund-only. URL to the external cap table.
        effective_date:
          type: string
          format: date
          nullable: true
          description: Fund-only. ISO date (YYYY-MM-DD).
        first_close_date:
          type: string
          format: date
          nullable: true
          description: Fund-only. ISO date (YYYY-MM-DD).
        fund_life_years:
          type: number
          format: double
          nullable: true
          description: Fund-only. Expected fund lifetime in years; 0.0 when unset.
    EntityShowResponse:
      description: Combined identity + details payload returned by `GET /api/v1/entities/{id}`,
        `POST /api/v1/entities`, and `PATCH /api/v1/entities/{id}`. The `calculations`
        block is only present on the GET endpoint when `?include=calculations` is
        supplied.
      allOf:
      - "$ref": "#/components/schemas/Entity"
      - type: object
        required:
        - details
        properties:
          details:
            "$ref": "#/components/schemas/EntityDetails"
          calculations:
            "$ref": "#/components/schemas/EntityCalculations"
    EntityCreateRequest:
      type: object
      description: |
        Request body for `POST /api/v1/entities`. `kind` and `name` are required;
        all other fields are optional. Fund-only fields (everything below
        `is_active`) are accepted only when `kind` is `fund`, `spv`, or
        `holding`; sending them on a non-fund kind returns 422.
      required:
      - kind
      - name
      properties:
        kind:
          type: string
          enum:
          - fund
          - spv
          - holding
          - management_company
          - general_partnership
        name:
          type: string
          description: Display name. Used for the new Organization, the entity_name
            on the kind-specific details row, and downstream callback names (DataRoom,
            etc.).
        is_active:
          type: boolean
          default: true
          description: When false, the new entity is created with `hidden_from_left_nav=true`.
        fiscal_year_end:
          type: string
          description: "`MM-DD`; DD must be the canonical last day of MM (e.g. `12-31`,
            `03-31`, `02-28`). Fund kinds only."
          example: 12-31
        gp_carry:
          type: string
          description: Decimal carry rate stored on `fund_details.carry_percentage`
            (e.g. `'0.20'` = 20%). Fund kinds only.
          example: '0.20'
        thesis:
          type: string
          description: Free-text investment thesis. Fund kinds only. Writing this
            field triggers async industry/region tagging.
        target_fund_size:
          type: integer
          minimum: 0
          description: Whole-dollar target raise (0 = unset). Fund kinds only.
        description:
          type: string
          description: Public-facing fund description. Fund kinds only.
        news:
          type: string
          description: LP-facing news/announcements. Fund kinds only.
        deck_url:
          type: string
          description: URL to the fund pitch deck. Fund kinds only.
        cap_table_link:
          type: string
          description: URL to the external cap table. Fund kinds only.
        effective_date:
          type: string
          format: date
          description: ISO date (YYYY-MM-DD). Fund kinds only.
        first_close_date:
          type: string
          format: date
          description: ISO date (YYYY-MM-DD). Fund kinds only.
        fund_life_years:
          type: number
          minimum: 0
          description: Expected fund lifetime in years. Fund kinds only.
    EntityUpdateRequest:
      type: object
      description: |
        Request body for `PATCH /api/v1/entities/{id}`. All fields are optional;
        only fields explicitly sent are applied. `kind`, if present, MUST match
        the entity's current kind (sending a differing value returns 422) — the
        entity kind is immutable post-create. Fund-only fields are rejected
        with 422 when the target entity is a `management_company` or
        `general_partnership`.
      properties:
        kind:
          type: string
          description: Must match the entity's current kind if supplied. Sending a
            different kind returns 422.
          enum:
          - fund
          - spv
          - holding
          - management_company
          - general_partnership
        name:
          type: string
          description: New display name. Applied to the entity's Organization and
            (for fund/management_company/general_partnership kinds) mirrored into
            the corresponding `*_details.entity_name`.
        is_active:
          type: boolean
          description: When set, flips `hidden_from_left_nav` (true => visible =>
            hidden_from_left_nav=false).
        fiscal_year_end:
          type: string
          description: "`MM-DD`; same format rules as on create. Fund kinds only."
        gp_carry:
          type: string
          description: Decimal carry rate. Fund kinds only.
        thesis:
          type: string
          description: Fund kinds only.
        target_fund_size:
          type: integer
          minimum: 0
          description: Fund kinds only.
        description:
          type: string
          description: Fund kinds only.
        news:
          type: string
          description: Fund kinds only.
        deck_url:
          type: string
          description: Fund kinds only.
        cap_table_link:
          type: string
          description: Fund kinds only.
        effective_date:
          type: string
          format: date
          description: Fund kinds only.
        first_close_date:
          type: string
          format: date
          description: Fund kinds only.
        fund_life_years:
          type: number
          minimum: 0
          description: Fund kinds only.
    EntityCalculations:
      description: |
        Fund-level dashboard values, present on `GET /api/v1/entities/{id}` only
        when `?include=calculations` is supplied.

        Exactly one of two shapes — see `EntityCalculationsPopulated` and
        `EntityCalculationsSuppressed` below for the full field lists, which the
        docs renderer does not inline onto this entry.

        ALWAYS branch on `available` rather than assuming the figure groups are
        present. It is `false` for SPV entities whose computed fund-level figures
        do not reconcile with their actual deal economics; the response then
        carries `reason` and `message` and NONE of the figure groups. No computed
        fund-level figure is reported for such an entity, and none may be
        estimated or reconstructed from other endpoints and presented as one. The
        `details` block is unaffected by this suppression and still carries that
        entity's own configured values, including `target_fund_size`,
        `gp_carry`, `fund_life_years` and `first_close_date` — those are
        reported inputs, not computed figures, and must never be presented as
        this entity's ACTUAL capital, performance or fee figures.
        Every entity of `kind=spv` is suppressed — both legacy SPVs and Start
        SPVs — so a `kind=spv` entity never carries computed fund-level figures.
        Still branch on the flag rather than on `kind`; the flag is the contract.
      oneOf:
      - "$ref": "#/components/schemas/EntityCalculationsPopulated"
      - "$ref": "#/components/schemas/EntityCalculationsSuppressed"
    EntityCalculationsPopulated:
      type: object
      description: |
        The populated shape. Every figure group below is present together
        whenever `available` is `true`. Numeric values are decimal strings
        (currency 2dp, ratios 4dp); investor counts are integers. Per-window
        field presence: `capital_activity.invested` and `fees.reserved_for_fees`
        appear only when `window=now`.
      required:
      - available
      - fund
      - commitments
      - capital_activity
      - fees
      - investable_capital
      - performance
      - portfolio
      - investors
      - meta
      properties:
        available:
          type: boolean
          enum:
          - true
          description: Always `true` on this shape.
        fund:
          "$ref": "#/components/schemas/EntityCalculationsFund"
        commitments:
          "$ref": "#/components/schemas/EntityCalculationsCommitments"
        capital_activity:
          "$ref": "#/components/schemas/EntityCalculationsCapitalActivity"
        fees:
          "$ref": "#/components/schemas/EntityCalculationsFees"
        investable_capital:
          type: string
          nullable: true
          description: 2dp decimal string. Window-specific (now vs lifetime).
        performance:
          "$ref": "#/components/schemas/EntityCalculationsPerformance"
        portfolio:
          "$ref": "#/components/schemas/EntityCalculationsPortfolio"
        investors:
          "$ref": "#/components/schemas/EntityCalculationsInvestors"
        meta:
          "$ref": "#/components/schemas/EntityCalculationsMeta"
    EntityCalculationsSuppressed:
      type: object
      description: |
        The suppressed shape, returned with HTTP 200 (not 404) for an SPV entity
        whose computed fund-level figures do not reconcile. Carries no figure
        group at all — a null figure would read as zero to a consumer relaying it.
        `additionalProperties: false` makes that guarantee schema-enforced rather
        than prose; the populated branch stays open so it can gain fields without
        a breaking schema change.
      additionalProperties: false
      required:
      - available
      - reason
      - message
      properties:
        available:
          type: boolean
          enum:
          - false
          description: Always `false` on this shape.
        reason:
          type: string
          description: |
            Machine-readable suppression cause. Currently only
            `not_available_for_entity_type` is emitted, but treat this as an open
            string: further causes may be added without a major version, so match
            the ones you handle and fall back to `message` for anything else.
        message:
          type: string
          description: |
            Human/LLM-readable explanation of the suppression, including what may
            not be inferred or reconstructed in its place.
    EntityCalculationsFund:
      type: object
      required:
      - target_fund_size
      - fund_size
      - general_partnership_name
      properties:
        target_fund_size:
          type: string
          nullable: true
          description: 2dp decimal string.
        fund_size:
          type: string
          nullable: true
          description: 2dp decimal string. Currently mirrors `target_fund_size` (falls
            back to 0 when unset).
        general_partnership_name:
          type: string
          nullable: true
          description: Display name of the parent general partnership entity, when
            present.
    EntityCalculationsCommitments:
      type: object
      required:
      - total_committed
      - lp_commitments
      - committed
      properties:
        total_committed:
          type: string
          nullable: true
          description: "`account_organization.fund_total_commitment`. 2dp decimal
            string."
        lp_commitments:
          type: string
          nullable: true
          description: LP commitments, excluding the GP. 2dp decimal string.
        committed:
          type: string
          nullable: true
          description: |
            Window-specific. `window=now` returns committed capital as of
            today; `window=lifetime` returns the forecast committed capital.
            2dp decimal string.
    EntityCalculationsCapitalActivity:
      type: object
      description: |
        `invested` is present only when `window=now` — it is dropped entirely
        from the response (not returned as null) when `window=lifetime`.
      required:
      - called
      - contributed
      - paid_in
      properties:
        called:
          type: string
          nullable: true
          description: Total amount called across active capital accounts. 2dp decimal
            string.
        contributed:
          type: string
          nullable: true
          description: Capital contributions. 2dp decimal string.
        paid_in:
          type: string
          nullable: true
          description: Total paid in across capital accounts. 2dp decimal string.
        invested:
          type: string
          nullable: true
          description: Present only when `window=now`. Capital invested. 2dp decimal
            string.
    EntityCalculationsFees:
      type: object
      description: |
        `reserved_for_fees` is present only when `window=now` — it is dropped
        entirely from the response (not returned as null) when `window=lifetime`.
        `total_fees` is `management_fees + admin_fees + other_fees` for the chosen
        window (realized fee deductions only) and does NOT include `reserved_for_fees`
        even on the `now` window — those are a forward-looking reservation, not a
        realized deduction. Fees are emitted as negative values per project convention.

        `basis` labels each field's recognition basis SEPARATELY because the
        calculation paths do not share one, and only `accrued_to_date` fields are
        expense already incurred; the rest are forward-looking projections or
        reserves. No value in this group is cash disbursed to the GP. Neither an
        accrued-but-unpaid (GP payable) amount nor a fees-paid-to-date amount is
        available from this endpoint, and no combination of these fields yields
        one. `basis` and `note` restate this inline so consumers reading only the
        payload (LLM tool calls in particular) get the semantics with the numbers.
      required:
      - total_fees
      - management_fees
      - admin_fees
      - other_fees
      - basis
      - note
      properties:
        total_fees:
          type: string
          nullable: true
          description: |
            `management_fees + admin_fees + other_fees` for the chosen window.
            Realized fee deductions only. `reserved_for_fees` is excluded.
            2dp decimal string.
        management_fees:
          type: string
          nullable: true
          description: Window-specific. 2dp decimal string.
        admin_fees:
          type: string
          nullable: true
          description: Window-specific. 2dp decimal string.
        other_fees:
          type: string
          nullable: true
          description: Window-specific. 2dp decimal string.
        reserved_for_fees:
          type: string
          nullable: true
          description: |
            Present only when `window=now`. A forward-looking reserve: not a fee
            incurred, not an amount owed to the GP, and never an
            accrued-but-unpaid balance. Excluded from `total_fees`. What it
            measures depends on the fund path, so read `basis.reserved_for_fees`
            alongside it. 2dp decimal string.
        basis:
          type: object
          description: |
            PER-FIELD recognition basis, one entry per numeric field emitted in
            this group. It is deliberately not a single group-wide label: the
            calculation paths do not share one period. On `window=now` a start
            fund takes `management_fees` / `admin_fees` from the LIFETIME
            calculation while its `other_fees` are to date, so any one label
            would be false for some field on some fund. `accrued_to_date` marks
            expense already incurred; every other value marks a forward-looking
            projection or reserve, which is NOT incurred expense. No value in the
            group is cash disbursed.
          required:
          - total_fees
          - management_fees
          - admin_fees
          - other_fees
          properties:
            total_fees:
              type: string
              enum:
              - accrued_to_date
              - forecast_lifetime
              - mixed
              description: |
                The shared basis of `management_fees`, `admin_fees` and
                `other_fees` when they agree, otherwise `mixed`. `mixed` occurs
                on a start fund with `window=now` and means `total_fees` adds
                figures recognized over different periods.
            management_fees:
              type: string
              enum:
              - accrued_to_date
              - forecast_lifetime
            admin_fees:
              type: string
              enum:
              - accrued_to_date
              - forecast_lifetime
            other_fees:
              type: string
              enum:
              - accrued_to_date
              - forecast_lifetime
            reserved_for_fees:
              type: string
              enum:
              - next_12_months_fee_reserve
              - projected_lifetime_other_fees
              description: |
                Present only when `window=now`, alongside `reserved_for_fees`.
                `next_12_months_fee_reserve` on a standard fund (fees accruing
                over the next 12 months, floored at 10,000, via
                `InvestableCapitalService#calculate_reserved_for_fees`).
                `projected_lifetime_other_fees` on a start fund (via
                `calculate_other_fees('lifetime')`), an other-fees projection and
                not a management or admin fee reserve. Forward-looking either way.
        note:
          type: string
          description: |
            Human and LLM readable restatement of the fee semantics, emitted
            with the numbers so a consumer that never loaded this schema still
            sees them. Assembled from the same per-field bases: it always states
            the accrual-not-cash and no-payable rules, adds a warning when
            `basis.total_fees` is `mixed`, and on `window=now` explains
            `reserved_for_fees` with the wording matching its basis.
    EntityCalculationsPerformance:
      type: object
      required:
      - net_irr
      properties:
        net_irr:
          type: string
          nullable: true
          description: |
            Fund-wide net IRR. 4dp decimal
            string. Null when the underlying cash flows can't produce a finite rate.
    EntityCalculationsPortfolio:
      type: object
      required:
      - fmv
      - fmv_cap_adjusted
      properties:
        fmv:
          type: string
          nullable: true
          description: Portfolio fair-market value. 2dp decimal string.
        fmv_cap_adjusted:
          type: string
          nullable: true
          description: Cap-adjusted portfolio FMV. 2dp decimal string.
    EntityCalculationsInvestors:
      type: object
      required:
      - lp_total_count
      - lp_active_count
      properties:
        lp_total_count:
          type: integer
          description: Total LP capital-account count for the fund.
        lp_active_count:
          type: integer
          description: Count of LP capital accounts with `is_capital_account_enabled=true`.
    EntityCalculationsMeta:
      type: object
      required:
      - window
      - as_of
      - currency
      properties:
        window:
          type: string
          enum:
          - now
          - lifetime
          description: Echo of the requested window.
        as_of:
          type: string
          format: date
          description: ISO date the calculation values were computed against.
        currency:
          type: string
          description: Entity's currency, falling back to `USD`.
    JournalEntryCreateRequest:
      type: object
      description: Request body for creating a single journal entry (double-entry
        GL posting). `debit_code` and `credit_code` are GL account codes that must
        differ and both resolve to a known accounting account. `amount` must be numeric
        and greater than 0. `transaction_at` is an ISO date (YYYY-MM-DD). An optional
        counterparty is attached via the `counterparty_type` + `counterparty_id` pair.
      required:
      - debit_code
      - credit_code
      - amount
      - transaction_at
      properties:
        debit_code:
          type: string
          description: GL account code for the debit side (e.g. "1100"). Must differ
            from credit_code.
        credit_code:
          type: string
          description: GL account code for the credit side (e.g. "1000"). Must differ
            from debit_code.
        amount:
          type: string
          description: Transaction amount as a numeric string greater than 0.
        transaction_at:
          type: string
          format: date
          description: Effective date of the entry (ISO date YYYY-MM-DD).
        comment:
          type: string
          nullable: true
          description: Optional free-text note.
        counterparty_type:
          type: string
          nullable: true
          description: Optional polymorphic counterparty type (FirmAdmin::CapitalAccount,
            FirmAdmin::PortfolioCompany, Organization, Person). Requires counterparty_id.
        counterparty_id:
          type: integer
          nullable: true
          description: Optional counterparty id; only meaningful alongside counterparty_type.
    JournalEntryBulkCreateRequest:
      type: object
      description: Request body for atomically creating many journal entries under
        one entity. `journal_entries` holds 1 to 100 rows, each a full independent
        entry with the same shape and rules as JournalEntryCreateRequest. Every row
        is validated before any is created; if any row is invalid the whole batch
        is rejected and nothing is created.
      required:
      - journal_entries
      properties:
        journal_entries:
          type: array
          minItems: 1
          maxItems: 100
          description: 1 to 100 journal entry rows, validated all-or-nothing.
          items:
            "$ref": "#/components/schemas/JournalEntryCreateRequest"
    JournalEntryBulkErrors:
      type: object
      description: Per-row validation failures for a bulk create. Each entry identifies
        the zero-based row `index` plus the offending `field` and a human-readable
        `message`. When present the batch created nothing.
      required:
      - errors
      properties:
        errors:
          type: array
          items:
            type: object
            required:
            - index
            - field
            - message
            properties:
              index:
                type: integer
                description: Zero-based index of the offending row in the request
                  array.
              field:
                type: string
              message:
                type: string
    BankTransaction:
      type: object
      description: Privacy-safe imported bank transaction. Internal provider and banking
        identifiers are omitted.
      required:
      - id
      - entity
      - transaction_at
      - posted_at
      - transaction_amount
      - currency
      - provider
      - status
      - name
      - description
      - transaction_type
      - customer_ref
      - bank_account
      - journal_entry
      properties:
        id:
          type: integer
        entity:
          type: object
          required:
          - id
          - name
          properties:
            id:
              type: integer
            name:
              type: string
        transaction_at:
          type: string
          format: date-time
          nullable: true
        posted_at:
          type: string
          format: date-time
          nullable: true
        transaction_amount:
          type: string
          description: Signed decimal amount; negative is an outflow.
          nullable: true
        currency:
          type: string
        provider:
          type: string
          enum:
          - plaid
          - mercury
          - grasshopper_treasury_prime
          - citizens_fdx
          - svb
        status:
          type: string
          enum:
          - pending
          - posted
          - void
          - superseded
        name:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        transaction_type:
          type: string
          nullable: true
        customer_ref:
          type: string
          nullable: true
        bank_account:
          type: object
          nullable: true
          required:
          - id
          - name
          properties:
            id:
              type: integer
            name:
              type: string
        journal_entry:
          type: object
          nullable: true
          required:
          - id
          - reconciled
          - pending
          properties:
            id:
              type: integer
            reconciled:
              type: boolean
            pending:
              type: boolean
    BankTransactionListResponse:
      type: object
      required:
      - bank_transactions
      - page
      - per_page
      - total
      properties:
        bank_transactions:
          type: array
          items:
            "$ref": "#/components/schemas/BankTransaction"
        page:
          type: integer
        per_page:
          type: integer
        total:
          type: integer
    BankTransactionResponse:
      type: object
      required:
      - bank_transaction
      properties:
        bank_transaction:
          "$ref": "#/components/schemas/BankTransaction"
    JournalEntry:
      type: object
      description: A double-entry general-ledger posting (FirmAdmin::AccountingTransaction)
        under an entity. `transaction_amount` is the absolute amount (always positive;
        direction is determined by the debit/credit account assignment). `counterparty`
        is null when the entry has no counterparty.
      required:
      - id
      - transaction_at
      - transaction_amount
      - reconciled
      - pending
      - debit
      - credit
      properties:
        id:
          type: integer
        transaction_at:
          type: string
          format: date-time
          nullable: true
        transaction_amount:
          type: string
          description: Absolute amount as a decimal string.
          nullable: true
        reconciled:
          type: boolean
          nullable: true
        pending:
          type: boolean
          nullable: true
        comment:
          type: string
          nullable: true
        debit:
          type: object
          nullable: true
          description: Debit-side ledger account.
          properties:
            code:
              type: string
            name:
              type: string
        credit:
          type: object
          nullable: true
          description: Credit-side ledger account.
          properties:
            code:
              type: string
            name:
              type: string
        counterparty:
          type: object
          nullable: true
          description: Polymorphic counterparty (CapitalAccount, PortfolioCompany,
            Organization, ...).
          properties:
            type:
              type: string
            id:
              type: integer
            name:
              type: string
              nullable: true
    CapitalAccountIdentity:
      type: object
      description: Identity block shared by the list (`GET /api/v1/entities/{entity_id}/capital_accounts`,
        where each row extends it as `CapitalAccountListRow`) and show (`GET /api/v1/capital_accounts/{id}`)
        responses. The calculations endpoints do NOT include this block — single-CA
        returns only the `commitments`/`period_activity`/ `performance`/`meta` groups,
        and bulk-calc rows carry only `id` and `name` alongside the four calculation
        groups.
      required:
      - id
      - name
      - vehicle_type
      - partner_type
      - lpa_signed
      - is_capital_account_enabled
      - is_gp_carry
      properties:
        id:
          type: integer
        name:
          type: string
          description: Capital account display name.
        vehicle_type:
          type: string
          nullable: true
        partner_type:
          type: string
          enum:
          - limited_partner
          - general_partner
        admitted_on:
          type: string
          format: date
          nullable: true
        lp_signature_at:
          type: string
          format: date-time
          nullable: true
        lpa_signed:
          type: boolean
          description: True when the LPA has a timestamped signature or the imported
            Cornerstone signed flag is set.
        is_capital_account_enabled:
          type: boolean
        kyc_passed:
          type: boolean
          nullable: true
        aml_passed:
          type: boolean
          nullable: true
        is_gp_carry:
          type: boolean
        fund:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            name:
              type: string
    CapitalAccountListRow:
      description: One row of the `GET /api/v1/entities/{entity_id}/capital_accounts`
        response — the shared identity block plus the per-account commitment sum.
        List-only; the show response does not carry a top-level `committed_capital`
        (its commitment detail lives in the `commitments` array).
      allOf:
      - "$ref": "#/components/schemas/CapitalAccountIdentity"
      - type: object
        required:
        - committed_capital
        properties:
          committed_capital:
            type: string
            description: |
              Decimal string. The account's commitment tranches summed
              (e.g. `"500000.0"`); `"0.0"` when the account has no
              commitments.
    Commitment:
      type: object
      required:
      - id
      - committed_capital
      - committed_at
      properties:
        id:
          type: integer
        committed_capital:
          type: integer
          format: int64
          description: Whole-currency amount (no decimals — column is `bigint`).
        committed_at:
          type: string
          format: date-time
          nullable: true
    PrimaryContact:
      type: object
      nullable: true
      description: Primary contact for the capital account; null when not set.
      properties:
        id:
          type: integer
        person_id:
          type: integer
          nullable: true
          description: Person id; matches `/pacts` `person_id` for signer reconciliation.
        name:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
    CapitalAccountContact:
      type: object
      description: |
        A contact on a capital account. `contact_status_type` is the
        mailing/communication role; the boolean flags describe the legal
        relationship to the account (joint primary, spouse, trustee,
        entity owner/signatory, trust beneficiary). Multiple booleans can
        be true for the same person.
      required:
      - id
      - person_id
      - contact_status_type
      - is_primary_individual
      - is_spouse
      - is_trustee
      - is_trust_beneficiary
      - is_entity_owner
      - is_entity_signatory
      properties:
        id:
          type: integer
        person_id:
          type: integer
          nullable: true
          description: Person id; matches `/pacts` `person_id` for signer reconciliation.
        name:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
        phone:
          type: string
          nullable: true
        title:
          type: string
          nullable: true
        contact_status_type:
          type: string
          enum:
          - none
          - primary
          - additional
        is_primary_individual:
          type: boolean
          nullable: true
        is_spouse:
          type: boolean
          nullable: true
        is_trustee:
          type: boolean
          nullable: true
        is_trust_beneficiary:
          type: boolean
          nullable: true
        is_entity_owner:
          type: boolean
          nullable: true
        is_entity_signatory:
          type: boolean
          nullable: true
    EntityRef:
      type: object
      nullable: true
      required:
      - id
      - name
      - kind
      properties:
        id:
          type: integer
        name:
          type: string
        kind:
          type: string
          enum:
          - fund
          - spv
          - holding
          - management_company
          - general_partnership
          - unknown
    AccountingAccount:
      type: object
      description: |
        A chart-of-accounts entry / GL account code, shared across the account.
        `code` drives debit/credit automation and follows standard GL ranges
        (1xxx=assets, 2xxx=liabilities, 3xxx=equity, 4xxx=revenue,
        6xxx/7xxx=expenses).
      required:
      - id
      - code
      - name
      - description
      - is_1099_eligible
      properties:
        id:
          type: integer
        code:
          type: string
          nullable: true
          description: Ledger account code, e.g. "1000" (bank), "1100" (investments),
            "6000" (mgmt fees).
        name:
          type: string
          nullable: true
          description: Human-readable account name, e.g. "Cash & Cash Equivalents".
        description:
          type: string
          nullable: true
          description: Optional long-form description of the account purpose.
        is_1099_eligible:
          type: boolean
          nullable: true
          description: True if transactions against this account are eligible for
            1099 reporting.
    CapitalCallHeader:
      type: object
      description: |
        Header view of a capital call. `amount_being_called` is the sum of
        the per-LP amounts being called. `total_wired`, `total_outstanding`,
        `total_variance`, and `percentage_funded` are derived from reconciled
        capital-call payment transactions within the call's
        `created_at..(closed_at || now)` window. `fund_total_commitment` is the
        sum of active commitments across the parent fund's capital accounts
        (constant across all rows on an index page, since a page is scoped to
        a single fund).

        Every money figure here covers EVERY LP row on the call. It is never
        narrowed by `lp_query` and never shrunk by the embedded `lp_details`
        cap — see `lp_details_coverage` on the index row.
      required:
      - id
      - name
      - status
      - percentage_to_call
      - notes
      - pipeline_id
      - amount_being_called
      - total_wired
      - total_outstanding
      - total_variance
      - percentage_funded
      - fund_total_commitment
      - created_at
      - standing_call
      - closed_at
      - due_date
      - entity
      properties:
        id:
          type: integer
        name:
          type: string
        status:
          type: string
          enum:
          - draft
          - open
          - closed
        percentage_to_call:
          type: number
          format: float
        notes:
          type: string
          nullable: true
        pipeline_id:
          type: integer
          nullable: true
          description: Id of the capital-call communication pipeline. Nullable on
            draft calls created before the pipeline is provisioned.
        amount_being_called:
          type: string
          description: Decimal string sum across per-LP details.
        total_wired:
          type: string
          description: Decimal string sum of reconciled capital-call payment transactions
            whose `transaction_at` falls in `created_at..(closed_at || now)`.
        total_outstanding:
          type: string
          description: "`amount_being_called - total_wired` as a decimal string. Positive
            when LPs still owe money, negative when they have overpaid in aggregate."
        total_variance:
          type: string
          description: "`total_wired - amount_being_called` as a decimal string. Same
            orientation as each LP's `variance`, and exactly the sum of the per-LP
            variances across the whole call, so a per-LP roster reconciles against
            it without a sign flip. It is the negation of `total_outstanding`."
        percentage_funded:
          type: number
          format: float
          nullable: true
          description: "`total_wired / amount_being_called` as a float (0.0–1.0).
            Null when `amount_being_called` is zero."
        fund_total_commitment:
          type: string
          description: Sum of `committed_capital` across active commitments for the
            parent fund. Decimal string.
        created_at:
          type: string
          format: date-time
          description: |
            When the CALL was created — NOT when any LP was called. On a
            `standing_call` fund this is the fund's setup date and precedes
            every LP's signature. Read each LP's `called_at` for the LP-level
            timestamp.
        standing_call:
          type: boolean
          description: |
            True when the parent fund is a start fund / start SPV, which
            carries ONE standing 100% call for the life of the fund. LPs are
            added to that pre-existing call as they sign, so `created_at`
            precedes LP admissions rather than following them.
        closed_at:
          type: string
          format: date-time
          nullable: true
        due_date:
          type: string
          format: date
          nullable: true
        entity:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            name:
              type: string
    CapitalCallDetail:
      type: object
      description: Per-LP line item under a capital call.
      required:
      - id
      - capital_account
      - amount_being_called
      - called_at
      - committed_capital
      - wired_amount
      - wire_date
      - variance
      - payment_status
      - prospect_id
      properties:
        id:
          type: integer
        capital_account:
          type: object
          nullable: true
          properties:
            id:
              type: integer
            name:
              type: string
        amount_being_called:
          type: string
        called_at:
          type: string
          format: date-time
          description: |
            When THIS LP was added to the call. The only LP-level call
            timestamp — the parent call's `created_at` is when the CALL was
            created and, on a `standing_call` fund, precedes every LP's
            signature.
        committed_capital:
          type: string
        wired_amount:
          type: string
        wire_date:
          type: string
          format: date-time
          nullable: true
        variance:
          type: string
          description: wired_amount − amount_being_called (decimal string).
        payment_status:
          type: string
          enum:
          - fully_paid
          - underpaid
          - overpaid
          - unpaid
          - prepaid
          - over_commitment
          - any_variance
          - unknown
        prospect_id:
          type: integer
          nullable: true
          description: Id of this LP's prospect on the parent call's pipeline. Null
            when the prospect has been removed or never provisioned.
    CapitalAccountTransfer:
      type: object
      description: Transfer entry oriented at the requested capital account. `effective_at`
        is aliased from the underlying `transferred_at` column.
      required:
      - id
      - kind
      - from_capital_account_id
      - to_capital_account_id
      - amount
      properties:
        id:
          type: integer
        kind:
          type: string
          enum:
          - outbound
          - inbound
          description: "`outbound` when the requested CA is the `from_capital_account`;
            `inbound` otherwise."
        effective_at:
          type: string
          format: date-time
          description: Aliased from the underlying `transferred_at` column (`NOT NULL`).
        from_capital_account_id:
          type: integer
        to_capital_account_id:
          type: integer
        amount:
          type: integer
          format: int64
          description: Whole-currency amount (no decimals — column is `bigint`).
    PeriodCommitments:
      type: object
      description: Commitment + paid-in figures. All values are decimal strings; currency
        fields use 2 decimal places, ratio fields use 4. Nil values are returned as
        `null`.
      properties:
        committed_capital:
          type: string
          nullable: true
        total_committed_capital:
          type: string
          nullable: true
          description: Fund-wide aggregate of commitment tranches whose capital account
            had a signed LPA or was enabled and admitted by period_end (NOT scoped
            by `active`/`partner_type` filters in the bulk endpoint). Excludes PACT-only
            and unconfirmed prospect commitments.
        lp_committed_capital:
          type: string
          nullable: true
          description: Fund-wide LP share of commitment tranches whose capital account
            had a signed LPA or was enabled and admitted by period_end. Excludes PACT-only
            and unconfirmed prospect commitments.
        gp_committed_capital:
          type: string
          nullable: true
          description: Fund-wide GP share of commitment tranches whose capital account
            had a signed LPA or was enabled and admitted by period_end.
        percentage_of_total_commitments:
          type: string
          nullable: true
          description: Ratio (4 decimal places). Null in `totals` and for PACT-only
            or unconfirmed rows not signed or admitted by period_end.
        percentage_of_lp_commitments:
          type: string
          nullable: true
          description: Ratio. Null in `totals` and for PACT-only or unconfirmed rows
            not signed or admitted by period_end.
        percentage_of_funds_called:
          type: string
          nullable: true
          description: Ratio. Null in `totals`.
        amount_paid_in_for_capital_accounts:
          type: string
          nullable: true
        amount_paid_in_for_capital_accounts_percentage:
          type: string
          nullable: true
          description: Ratio. Null in `totals`.
        outstanding_capital_contribution:
          type: string
          nullable: true
        capital_call_receivable:
          type: string
          nullable: true
        capital_call_prepaid:
          type: string
          nullable: true
    PeriodActivity:
      type: object
      description: Per-period ledger movements. All values are decimal strings (currency,
        2 decimal places); null when not applicable.
      properties:
        beginning_balance:
          type: string
          nullable: true
        capital_contribution:
          type: string
          nullable: true
        syndication_cost:
          type: string
          nullable: true
        management_fees:
          type: string
          nullable: true
        incentive_fees:
          type: string
          nullable: true
        net_operating_gain:
          type: string
          nullable: true
        late_interest:
          type: string
          nullable: true
        unrealized_gain:
          type: string
          nullable: true
        carried_interest_accrued:
          type: string
          nullable: true
        net_realized_gain:
          type: string
          nullable: true
        deemed_gain:
          type: string
          nullable: true
        distributions:
          type: string
          nullable: true
        in_kind_distributions:
          type: string
          nullable: true
        transfers:
          type: string
          nullable: true
        ending_balance:
          type: string
          nullable: true
    PeriodPerformance:
      type: object
      description: Performance ratios. Decimal strings (4 decimal places). In the
        bulk endpoint's `totals` block these are always `null` because per-CA ratios
        don't aggregate cleanly across capital accounts.
      properties:
        dpi:
          type: string
          nullable: true
        rvpi:
          type: string
          nullable: true
        tvpi:
          type: string
          nullable: true
    PeriodMeta:
      type: object
      required:
      - period_start
      - period_end
      - currency
      - management_fee_system
      - carry_system
      - cutover_dates
      properties:
        period_start:
          type: string
          format: date
        period_end:
          type: string
          format: date
        currency:
          type: string
          example: USD
        management_fee_system:
          type: string
          enum:
          - legacy
          - new
          - hybrid
          description: "`legacy` when the window ends before the management-fee cutover;
            `new` when it starts on/after; `hybrid` when it straddles. Funds without
            a cutover date are always `legacy`."
        carry_system:
          type: string
          enum:
          - legacy
          - new
          - hybrid
        cutover_dates:
          type: object
          required:
          - management_fee
          - carry
          properties:
            management_fee:
              type: string
              format: date
              nullable: true
            carry:
              type: string
              format: date
              nullable: true
    BulkCalculationsRow:
      type: object
      description: One row in the bulk-calculations response. Always has `id` and
        `name`. The `commitments`, `period_activity`, `performance`, and `meta` groups
        are present by default; supplying `?fields=` may drop entire groups from the
        response (a group with zero requested fields is omitted from both rows and
        totals).
      required:
      - id
      - name
      properties:
        id:
          type: integer
        name:
          type: string
        commitments:
          "$ref": "#/components/schemas/PeriodCommitments"
        period_activity:
          "$ref": "#/components/schemas/PeriodActivity"
        performance:
          "$ref": "#/components/schemas/PeriodPerformance"
        meta:
          "$ref": "#/components/schemas/PeriodMeta"
    BulkCalculationsTotals:
      type: object
      description: Fund-level totals across the filtered scope (NOT the current page).
        Per-CA ratio fields and the performance ratios (`dpi`, `rvpi`, `tvpi`) are
        always `null` in totals — per-account ratios do not sum into a fund-level
        figure. Currency fields are summed across the filtered scope. As with rows,
        supplying `?fields=` may drop entire groups from this object.
      properties:
        commitments:
          "$ref": "#/components/schemas/PeriodCommitments"
        period_activity:
          "$ref": "#/components/schemas/PeriodActivity"
        performance:
          "$ref": "#/components/schemas/PeriodPerformance"
        meta:
          "$ref": "#/components/schemas/PeriodMeta"
    AccessType:
      type: string
      enum:
      - public_access
      - private_access
    RsvpStatus:
      type: string
      enum:
      - 'no'
      - 'yes'
      - waiting_list
    TimeZone:
      type: string
      enum:
      - Africa/Algiers
      - Africa/Cairo
      - Africa/Casablanca
      - Africa/Harare
      - Africa/Johannesburg
      - Africa/Monrovia
      - Africa/Nairobi
      - America/Argentina/Buenos_Aires
      - America/Bogota
      - America/Caracas
      - America/Chicago
      - America/Chihuahua
      - America/Denver
      - America/Godthab
      - America/Guatemala
      - America/Guyana
      - America/Halifax
      - America/Indiana/Indianapolis
      - America/Juneau
      - America/La_Paz
      - America/Lima
      - America/Los_Angeles
      - America/Mazatlan
      - America/Mexico_City
      - America/Monterrey
      - America/Montevideo
      - America/New_York
      - America/Phoenix
      - America/Puerto_Rico
      - America/Regina
      - America/Santiago
      - America/Sao_Paulo
      - America/St_Johns
      - America/Tijuana
      - Asia/Almaty
      - Asia/Baghdad
      - Asia/Baku
      - Asia/Bangkok
      - Asia/Chongqing
      - Asia/Colombo
      - Asia/Dhaka
      - Asia/Hong_Kong
      - Asia/Irkutsk
      - Asia/Jakarta
      - Asia/Jerusalem
      - Asia/Kabul
      - Asia/Kamchatka
      - Asia/Karachi
      - Asia/Kathmandu
      - Asia/Kolkata
      - Asia/Krasnoyarsk
      - Asia/Kuala_Lumpur
      - Asia/Kuwait
      - Asia/Magadan
      - Asia/Muscat
      - Asia/Novosibirsk
      - Asia/Rangoon
      - Asia/Riyadh
      - Asia/Seoul
      - Asia/Shanghai
      - Asia/Singapore
      - Asia/Srednekolymsk
      - Asia/Taipei
      - Asia/Tashkent
      - Asia/Tbilisi
      - Asia/Tehran
      - Asia/Tokyo
      - Asia/Ulaanbaatar
      - Asia/Urumqi
      - Asia/Vladivostok
      - Asia/Yakutsk
      - Asia/Yekaterinburg
      - Asia/Yerevan
      - Atlantic/Azores
      - Atlantic/Cape_Verde
      - Atlantic/South_Georgia
      - Australia/Adelaide
      - Australia/Brisbane
      - Australia/Canberra
      - Australia/Darwin
      - Australia/Hobart
      - Australia/Melbourne
      - Australia/Perth
      - Australia/Sydney
      - Etc/GMT+12
      - Etc/UTC
      - Europe/Amsterdam
      - Europe/Athens
      - Europe/Belgrade
      - Europe/Berlin
      - Europe/Bratislava
      - Europe/Brussels
      - Europe/Bucharest
      - Europe/Budapest
      - Europe/Copenhagen
      - Europe/Dublin
      - Europe/Helsinki
      - Europe/Istanbul
      - Europe/Kaliningrad
      - Europe/Kiev
      - Europe/Lisbon
      - Europe/Ljubljana
      - Europe/London
      - Europe/Madrid
      - Europe/Minsk
      - Europe/Moscow
      - Europe/Paris
      - Europe/Prague
      - Europe/Riga
      - Europe/Rome
      - Europe/Samara
      - Europe/Sarajevo
      - Europe/Skopje
      - Europe/Sofia
      - Europe/Stockholm
      - Europe/Tallinn
      - Europe/Vienna
      - Europe/Vilnius
      - Europe/Volgograd
      - Europe/Warsaw
      - Europe/Zagreb
      - Europe/Zurich
      - Pacific/Apia
      - Pacific/Auckland
      - Pacific/Chatham
      - Pacific/Fakaofo
      - Pacific/Fiji
      - Pacific/Guadalcanal
      - Pacific/Guam
      - Pacific/Honolulu
      - Pacific/Majuro
      - Pacific/Midway
      - Pacific/Noumea
      - Pacific/Pago_Pago
      - Pacific/Port_Moresby
      - Pacific/Tongatapu
    associated_person:
      type: object
      properties:
        first_name:
          type: string
        middle_name:
          type: string
        last_name:
          type: string
        email:
          type: string
        phone:
          type: string
      required:
      - first_name
      - last_name
    address:
      type: object
      properties:
        street:
          type: string
        city:
          type: string
        state:
          type: string
        country:
          type: string
        zipcode:
          type: string
    person_update_attributes:
      type: object
      properties:
        first_name:
          type: string
        middle_name:
          type: string
        last_name:
          type: string
        email:
          type: string
        phone:
          type: string
        linkedin:
          type: string
        tag_list:
          type: string
          description: Comma-separated tags to add.
        remove_tag_list:
          type: string
          description: Comma-separated tags to remove.
        note:
          type: string
        picture:
          type: string
          description: Profile photo as a base64 data URI or public http(s) image
            URL.
        custom_data_points:
          type: object
        address:
          "$ref": "#/components/schemas/address"
        referred_by:
          "$ref": "#/components/schemas/associated_person"
        organizations:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              title:
                type: string
    organization_update_attributes:
      type: object
      properties:
        name:
          type: string
        website:
          type: string
        description:
          type: string
        note:
          type: string
        tag_list:
          type: string
          description: Comma-separated tags to add.
        remove_tag_list:
          type: string
          description: Comma-separated tags to remove.
        logo:
          type: string
          description: Logo as a base64 data URI or public http(s) image URL.
        traction:
          type: string
        custom_data_points:
          type: object
        address:
          "$ref": "#/components/schemas/address"
        referred_by:
          "$ref": "#/components/schemas/associated_person"
        people:
          type: array
          items:
            allOf:
            - "$ref": "#/components/schemas/associated_person"
            - type: object
              required:
              - email
    financial_report_job:
      type: object
      properties:
        id:
          type: integer
        status:
          type: string
          enum:
          - queued
          - running
          - succeeded
          - failed
        created_at:
          type: string
          format: date-time
        started_at:
          type: string
          format: date-time
          nullable: true
        finished_at:
          type: string
          format: date-time
          nullable: true
        status_url:
          type: string
          format: uri
        error_message:
          type: string
          nullable: true
        files:
          type: array
          description: One combined file per job. Each entry lists the report types
            included as worksheets / sections inside that file.
          items:
            type: object
            properties:
              report_types:
                type: array
                items:
                  type: string
                  enum:
                  - balance_sheet
                  - income_statement
                  - schedule_of_investments
                  - statement_of_changes
              filename:
                type: string
              content_type:
                type: string
              byte_size:
                type: integer
              download_url:
                type: string
                format: uri
                nullable: true
    variable:
      type: object
      description: |
        A merge-tag variable usable in email templates (e.g.
        `{{fund_thesis}}`). The `id` is opaque — pass it back to
        `GET /api/v1/variables/{id}` to look up the row.
      properties:
        id:
          type: string
          description: Opaque variable id. Treat as a string.
        name:
          type: string
        context:
          type: string
          nullable: true
          description: |
            Namespace string (e.g. `email`, `global`, `person_data_points`,
            `organization_data_points`). May be `null`.
        description:
          type: string
          nullable: true
        value:
          type: string
          nullable: true
          description: |
            Literal substitution content for variables that carry one
            (e.g. the fund thesis text for `{{email.fund_thesis}}`).
            `null` when the variable's value is resolved dynamically at
            send time.
        format_name:
          type: string
          nullable: true
          description: |
            Human-readable format name (e.g. `String`, `Paragraph`,
            `Currency (US)`, `Date (US)`, `Select (Dropdown)`, `URL`).
            `null` for policy-protected variables whose format is not
            statically known.
        display_format:
          type: string
          description: |
            Merge-tag form (e.g. `email.fund_thesis`). Wrap in `{{...}}`
            to substitute in an email template body.
        select_options:
          type: array
          nullable: true
          description: |
            Present only when `format_name == "Select (Dropdown)"`. Either
            a flat array of strings (label == value) or an array of
            `[label, value]` pairs.
          items:
            oneOf:
            - type: string
            - type: array
              items:
                type: string
      required:
      - id
      - name
      - context
      - format_name
      - display_format
    pagination:
      type: object
      properties:
        total_count:
          type: integer
        current_page:
          type: integer
        total_pages:
          type: integer
    Task:
      type: object
      description: A Hub Task with its lifecycle status, origin, and assignment.
      properties:
        id:
          type: integer
        title:
          type: string
        description:
          type: string
          nullable: true
        assigner_note:
          type: string
          nullable: true
          description: Optional free-text note from the assigner, rendered prominently
            to the assignee on the task page.
        status:
          type: string
          enum:
          - open
          - in_progress
          - completed
          - cancelled
        origin:
          type: string
          enum:
          - agent_suspension
          - user
          - system
        due_date:
          type: string
          format: date
          nullable: true
        deep_link_url:
          type: string
          nullable: true
        action_type:
          type: string
          enum:
          - open_link
          - review_approve
          - upload
          - sign
          - fill_form
          - verify_identity
          - make_payment
          - hub_action
          - provide_info
          - follow_up
          - schedule
          - provide_tax_info
        action_config:
          type: object
          description: Per-action-type config (shallow). Keys vary by action_type.
        subject_type:
          type: string
          nullable: true
          description: Polymorphic subject type the task is about, if any.
        subject_id:
          type: integer
          nullable: true
        assignee_id:
          type: integer
        created_by_user_id:
          type: integer
          nullable: true
        account_id:
          type: integer
        account_organization_id:
          type: integer
          nullable: true
        parent_task_id:
          type: integer
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
      - id
      - title
      - status
      - origin
      - assignee_id
      - account_id
