ClosedLoop AI Features API

**Features**: the buildable, shippable children of a theme. Filter by `theme_id`. Same theme → features hierarchy you see on the roadmap.

Operations 2

GET /features List / search features #
GET /features/{id} Get a feature #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/closedloop-features-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

closedloop-features-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: ClosedLoop AI Public Features API
  version: 1.8.0
  x-logo:
    url: https://app.closedloop.sh/favicon.svg
    altText: ClosedLoop AI
  description: '# ClosedLoop AI Public API


    Programmatic access to your team''s **product insights**, the structured intelligence

    ClosedLoop AI extracts from customer conversations (Gong, Fireflies, Slack, …) and

    structured feedback (surveys, webhooks).'
  contact:
    name: ClosedLoop AI Support
    email: support@closedloop.sh
    url: https://closedloop.sh
  license:
    name: Proprietary (ClosedLoop Labs LLC)
    url: https://closedloop.sh/terms
servers:
- url: https://api.closedloop.sh/v1
  description: United States (production)
- url: https://eu.api.closedloop.sh/v1
  description: European Union (production)
security:
- ApiKeyAuth: []
tags:
- name: Features
  description: '**Features**: the buildable, shippable children of a theme. Filter by `theme_id`.

    Same theme → features hierarchy you see on the roadmap.'
paths:
  /features:
    get:
      operationId: listFeatures
      tags:
      - Features
      summary: List / search features
      x-mint:
        metadata:
          description: List or search buildable features by theme, product, area, status, or text, with scoped evidence counts and deterministic sorting.
      description: 'Buildable features: the shippable children of a theme. Filter by `theme_id`,

        `product_id`, `feature_area_id`, `product_area_id`, `product_feature_id`,

        status, or search text.

        Product fields are null when a feature has not been assigned to a product.


        `feature_area_id` is a subject area from `/areas`, NOT a feature ID from this

        endpoint. The two are different kinds of ID: a feature ID identifies one

        buildable feature and is what `/features/{id}` takes, while a feature-area ID

        identifies the area an insight is filed under. `feature_area_id` therefore

        selects features by the area of their supporting insights.


        With an AREA filter — `feature_area_id`, `product_area_id`, or both —

        `insight_count` and `unique_customer_count` are RECOMPUTED from the distinct

        insights in that scope and the distinct customers behind them; features with

        no matching evidence are omitted, and `pagination.total` counts what is

        returned. `sort=insight_count` and `sort=unique_customer_count` then rank on

        those scoped values. `sort=ric_score` is unchanged by the filter: it stays the

        feature''s stored OVERALL score across every area, so a feature can outrank

        another that has more evidence in the selected scope. Every sort resolves ties

        on the immutable feature ID, so paging is deterministic. With NEITHER area

        filter the counts are the feature''s stored all-area totals. Every filter on

        this endpoint combines with AND.


        The default response is CURATED, not a census: each team has an adaptive

        size floor, computed nightly from that team''s own distribution, and very

        small items below it are excluded — the same set the ClosedLoop AI app

        shows. `pagination.total` counts the curated set. Set `show_all=true` for

        every row regardless of size. Teams with too little data to compute a

        floor are never filtered.


        Unlike `/themes`, this floor applies at every status: a feature is sized

        by its own insight count, which a finished feature still has. That size is the

        feature''s OVERALL insight count, so the floor is unaffected by the area

        filters and `show_all=true` does not widen the selected area. The floor and

        the published count therefore read different numbers: a returned feature can

        carry a scoped `insight_count` below the floor, because the floor judged its

        overall size, and a feature whose evidence sits entirely in the selected area

        is still hidden when its overall count is below the floor.'
      parameters:
      - name: show_all
        in: query
        schema:
          type: boolean
          default: false
        description: Include features below the team's adaptive size floor. Default false.
      - $ref: '#/components/parameters/Query'
      - name: search_mode
        in: query
        schema:
          type: string
          enum:
          - lexical
          - semantic
          default: lexical
        description: 'Lexical substring matching against the feature''s own title and

          description, or semantic matching against feature embeddings.


          Semantic search returns rows best-match-first and therefore OVERRIDES

          `sort`. Relevance order and size order are mutually exclusive: a caller

          relying on `sort=insight_count` for scope-aware ordering loses it when

          semantic is enabled.


          Semantic requests share ONE rate budget with `/themes`; they are not

          separately allowanced.

          '
      - name: min_similarity
        in: query
        schema:
          type: number
          format: float
          minimum: 0.3
          maximum: 1
          default: 0.5
        description: 'Minimum cosine similarity for semantic search. Valid only when

          search_mode is semantic.

          '
      - name: allow_fallback
        in: query
        schema:
          type: boolean
          default: false
        description: 'Permit falling back to lexical when an embedding cannot be generated.

          Default false, so such a request fails with 503 rather than silently

          returning a different result set. The response always reports the mode

          that actually ran.

          '
      - name: theme_id
        in: query
        schema:
          type: string
          format: uuid
        description: Return only the features under this theme.
      - $ref: '#/components/parameters/ProductId'
      - $ref: '#/components/parameters/FeatureAreaId'
      - name: product_area_id
        in: query
        schema:
          type: string
          format: uuid
        description: 'Return only features with supporting insights filed anywhere under this

          parent product area, with `insight_count` and `unique_customer_count`

          recomputed over exactly that evidence. This is `feature_area_id` one

          level up: a product area is the parent of many feature areas, so it is

          the coarser cut of the same question. Combines with every other filter

          using AND, so pairing it with `feature_area_id` narrows to that one

          child area — and yields nothing when that area sits under a different

          product area.


          ARCHIVED child areas are excluded, which `feature_area_id` does not do:

          naming an archived area directly still counts its evidence, while its

          parent product area does not. A product area whose children are all

          archived therefore returns an empty list. `/areas` with

          `include_archived=true` sums archived children into a product area''s

          `insight_count`, so that total can exceed what this filter returns.

          '
      - name: product_feature_id
        in: query
        schema:
          type: string
          format: uuid
        description: 'Return only the feature with this ID. On THIS endpoint the parameter

          names the listed resource itself, so it selects that one feature;

          on `/insights` and `/themes` the same parameter instead means

          "linked to this feature". Combines with every other filter using

          AND, so pairing it with `feature_area_id` answers "does this feature

          have evidence in that area, and how much" in one call. Use

          `/features/{id}` to fetch one feature with its evidence, severity

          breakdown and affected customers.

          '
      - name: status
        in: query
        schema:
          type: string
          enum:
          - active
          - declined
          - shipped
          - parked
      - name: sort
        in: query
        schema:
          type: string
          enum:
          - ric_score
          - insight_count
          - unique_customer_count
          - created_at
          default: ric_score
      - $ref: '#/components/parameters/Limit'
      - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: A page of features
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                - pagination
                - search_mode
                - min_similarity
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Feature'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  search_mode:
                    type: string
                    enum:
                    - lexical
                    - semantic
                    description: Search mode actually applied. Lexical indicates lexical/default search or an explicitly allowed fallback.
                  min_similarity:
                    type:
                    - number
                    - 'null'
                    format: float
                    description: Applied semantic threshold, or null when the applied mode was lexical.
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          $ref: '#/components/responses/AuthenticationUnavailable'
  /features/{id}:
    get:
      operationId: getFeature
      tags:
      - Features
      summary: Get a feature
      description: 'Feature detail with its evidence: severity breakdown, affected customers, and top

        supporting insights.'
      parameters:
      - $ref: '#/components/parameters/PathId'
      responses:
        '200':
          description: Feature detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeatureDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '503':
          $ref: '#/components/responses/AuthenticationUnavailable'
components:
  parameters:
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        default: 50
        minimum: 1
        maximum: 200
      description: Page size (max 200).
    ProductId:
      name: product_id
      in: query
      schema:
        type: string
        format: uuid
      description: Filter by one product ID from `/products`.
    PathId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Resource UUID.
    Offset:
      name: offset
      in: query
      schema:
        type: integer
        default: 0
        minimum: 0
      description: Number of records to skip.
    Query:
      name: q
      in: query
      schema:
        type: string
      description: Free-text search over titles and content.
    FeatureAreaId:
      name: feature_area_id
      in: query
      schema:
        type: string
        format: uuid
      description: 'Filter by one child feature-area ID from `/areas`. A feature area is the subject

        area an insight is filed under; it is NOT a buildable feature ID from

        `/features`, and the two are never interchangeable.

        '
  schemas:
    Error:
      type: object
      required:
      - error
      - code
      properties:
        error:
          type: string
          description: User-safe message.
        code:
          type: string
          example: INVALID_API_KEY
        hint:
          type: string
          description: Optional next step.
    Pagination:
      type: object
      required:
      - total
      - limit
      - offset
      properties:
        total:
          type: integer
          description: Total matching records (full set, not the page).
          example: 508
        limit:
          type: integer
          example: 50
        offset:
          type: integer
          example: 0
    Customer:
      type: object
      required:
      - id
      - name
      properties:
        id:
          type: string
          format: uuid
          example: c0ffee00-0000-4000-8000-000000000001
        name:
          type: string
          example: Acme Co
        parent_id:
          type:
          - string
          - 'null'
          format: uuid
          description: Parent account (account family), if this is a child property.
        parent_name:
          type:
          - string
          - 'null'
          description: Parent account name.
        domain:
          type: string
          example: acme.com
        industry:
          type: string
          example: Hospitality
        employee_count:
          type: integer
          example: 250
        arr:
          type:
          - number
          - 'null'
          example: 48000
          description: 'Annualized recurring revenue: SUM of won-deal annual_value (team currency).'
        currency:
          type: string
          example: USD
        is_active_customer:
          type:
          - boolean
          - 'null'
          example: true
          description: Tri-state. true = a confirmed active customer; false = resolved and not an active customer; null = the account could not be resolved, so the status is unknown. Never read null as false.
        churn_state:
          type: string
          enum:
          - active
          - at_risk
          - churned
          example: active
        insight_count:
          type: integer
          example: 23
        last_seen:
          type: string
          format: date-time
          example: '2026-06-10T09:00:00Z'
    FeatureDetail:
      allOf:
      - $ref: '#/components/schemas/Feature'
      - type: object
        properties:
          insight_count:
            type: integer
            description: Distinct supporting insights across every area, counted from the feature's current evidence. This detail response is never scoped by `feature_area_id`, and it can differ from the same field on `GET /features` without a filter, which reports the stored rollup.
          unique_customer_count:
            type: integer
            description: Distinct customers behind those insights, counted the same way as `insight_count` above.
          deal_blocker_count:
            type: integer
            description: Distinct customers represented by deal-blocker evidence.
          severity_breakdown:
            type: object
            example:
              critical: 0
              high: 3
              medium: 7
              low: 2
          top_insights:
            type: array
            items:
              $ref: '#/components/schemas/InsightSummary'
          affected_customers:
            type: array
            items:
              $ref: '#/components/schemas/Customer'
    Feature:
      type: object
      required:
      - id
      - theme_id
      - title
      - ric_score
      - status
      properties:
        id:
          type: string
          format: uuid
          example: b2c3d4e5-f6a7-4b8c-9d0e-000000000002
        theme_id:
          type: string
          format: uuid
          example: a1b2c3d4-e5f6-4a7b-8c9d-000000000001
          description: The parent theme.
        title:
          type: string
          example: Dark mode toggle in settings
        description:
          type: string
          example: A user-controllable dark / light toggle in the settings panel.
        ric_score:
          type: number
          example: 6.9
        insight_count:
          type: integer
          example: 12
          description: Supporting insights across every area. On `GET /features` with `feature_area_id`, this is instead recomputed from the insights in the selected area only. See `/features/{id}` for how the detail response derives it.
        unique_customer_count:
          type: integer
          example: 9
          description: Distinct customers behind those insights, scoped the same way as `insight_count`.
        product_id:
          type:
          - string
          - 'null'
          format: uuid
          description: Assigned product, if any.
        product_name:
          type:
          - string
          - 'null'
          example: Core Platform
        status:
          type: string
          enum:
          - active
          - declined
          - shipped
          - parked
          example: active
        created_at:
          type: string
          format: date-time
          example: '2026-04-05T00:00:00Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-06-01T00:00:00Z'
    InsightSummary:
      type: object
      description: Compact insight evidence embedded in theme and feature detail responses.
      required:
      - id
      - title
      - severity
      - source
      - created_at
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        category:
          type: string
        severity:
          type: string
          enum:
          - critical
          - high
          - medium
          - low
        customer_id:
          type:
          - string
          - 'null'
          format: uuid
        customer_name:
          type: string
        source:
          type: string
        source_date:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
  responses:
    Unauthorized:
      description: 'Missing or invalid API key. `code` is `NO_API_KEY` when the `X-API-Key`

        header is absent, or `INVALID_API_KEY` when a key is present but invalid

        or inactive.

        '
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missing:
              summary: No key provided
              value:
                error: API key required.
                code: NO_API_KEY
                hint: Send your key in the X-API-Key header.
            invalid:
              summary: Key invalid or inactive
              value:
                error: Invalid or inactive API key.
                code: INVALID_API_KEY
                hint: Create a key in Settings → API Keys.
    NotFound:
      description: Resource not found (or not in your team)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Not found.
            code: NOT_FOUND
    ValidationError:
      description: Invalid query parameter, filter combination, or cursor
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            uuid:
              summary: Invalid filter ID
              value:
                error: product_id must be a UUID.
                code: VALIDATION_ERROR
            cursor:
              summary: Invalid cursor
              value:
                error: cursor is invalid or expired
                code: VALIDATION_ERROR
    AuthenticationUnavailable:
      description: API-key authentication infrastructure is temporarily unavailable
      headers:
        Retry-After:
          schema:
            type: integer
            example: 10
          description: Wait 10 seconds before retrying authentication.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            authentication:
              summary: Authentication dependency failure
              value:
                error: API key authentication is temporarily unavailable. Please retry shortly.
                code: API_KEY_AUTH_UNAVAILABLE
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: 'Team-scoped API key created in the app (Settings → API Keys).

        Sent as `X-API-Key: <key>` on every request. The header name `apikey`

        is also accepted as an alias.

        '
x-tagGroups:
- name: Prioritization
  tags:
  - Insights
  - Products
  - Themes
  - Features
- name: Customers & Context
  tags:
  - Customers
  - Context
  - Competitors
- name: Reporting
  tags:
  - Analytics
- name: Account
  tags:
  - Integrations
  - Usage
- name: Meta
  tags:
  - Meta