Skip to main content

Shared behavior

Every /v1 endpoint follows the same rules, so what you learn on one endpoint applies to all of them. The API is read-only: every endpoint is a GET, and nothing is mutated. Feedback is ingested through your connected integrations, not this API.

Authentication

Send your key on every request as an X-API-Key header:
The header name apikey is also accepted as an alias for X-API-Key. Keys are team-scoped (every response is limited to your team’s data) and region-scoped (a key issued in the EU only works against https://eu.api.closedloop.sh/v1). Create keys in Settings, API Keys in the app. See API keys for the full lifecycle. API-key authentication is designed for unattended server workloads, including scheduled Databricks jobs and agents with no person present at run time.

Pagination

Offset pagination

List endpoints accept limit and offset: Responses wrap the page in data and include a pagination object whose total is the full-set count of matching records, not the size of the page:
Page through a result set by increasing offset until you have read total records.

Incremental insight export

For a repeatable /insights export, make the first request with updated_since instead of offset:
The response is ordered by updated_at, then id, and uses a fixed upper watermark:
Continue with only cursor and, optionally, a different limit:
The export window is updated_since < updated_at <= sync_until. Save sync_until as your next checkpoint only after the final page returns has_more: false. Upsert rows by immutable id; replaying the last committed window gives at-least-once recovery after a failed run. Treat cursors as opaque. They are signed and bound to the team, API key, filters, and watermark. If the key is rotated during a run, restart from the last committed sync_until with the new key. Cursor exports do not include a total because the fixed window and has_more determine completion.
Incremental export tracks changes to the insight row. A change made only to a product, product-feature, or product-area association may not advance the insight’s updated_at, so that association-only change may not appear until the insight itself changes. Deletions are not emitted as tombstones.

Filtering

Filters are query parameters, and multiple filters combine with AND semantics:
  • Free-text search: most list endpoints accept q, a free-text search over titles and content.
  • Date ranges: date_from and date_to are inclusive ISO 8601 dates.
  • Product scope: insights, themes, and coverage accept product_id, product_feature_id, product_area_id, and feature_area_id. Features accept product_id, feature_area_id, product_area_id and product_feature_id (which on that endpoint selects the named feature itself).
  • Resource-specific filters: each endpoint documents its own. For example, insights filter by category, severity, and customer_id; themes accept status and sort; features accept theme_id; context accepts type.
GET /themes?q=... uses lexical search by default: the complete q value must occur as one case-insensitive substring in the theme title or description. For meaning-based matches, use:
q accepts at most 500 characters. min_similarity is optional, defaults to 0.50, and accepts 0.30 through 1.00. Semantic results are ordered by similarity descending and then immutable theme ID, so semantic relevance overrides sort. If ClosedLoop AI cannot generate the query embedding, the request returns 503 SEMANTIC_SEARCH_UNAVAILABLE by default. To permit lexical fallback, add allow_fallback=true. Read the response’s search_mode and min_similarity: an allowed fallback returns "search_mode": "lexical" and "min_similarity": null. Call /products, /features, and /areas to discover the current product-scope IDs. Insight responses expose the same IDs so filters are round-trippable: Each product_features[] item also includes its resolved title and nullable product_id. Call /facets for categories, severities, and sources. product_feature_id and feature_area_id name two different kinds of ID and are never interchangeable. A product feature is a buildable item from /features, identified by a feature ID; /features/{id} takes that ID. A feature area is the subject area an insight is filed under, listed by /areas. GET /features?feature_area_id=... therefore selects features by the area of their supporting insights, and scopes each returned feature’s insight_count and unique_customer_count to that area; features with no evidence in the area are omitted. product_area_id does the same one level up, across every active child area of that product area. sort=ric_score stays the feature’s stored overall score in that case, while sort=insight_count and sort=unique_customer_count rank on the scoped values. Product filtering returns insights with a recorded association. Some insights remain unassigned when ClosedLoop AI cannot determine a reliable automatic match, so filtered results may not include every potentially relevant insight. Fully unassociated insights return both arrays empty. A feature can also appear with product_id: null; in that case products is empty while product_features is not.

Stable IDs

Insight and theme IDs are immutable UUIDs and are never reassigned or recycled to another resource. If a resource is removed, a later lookup returns 404; the API does not reuse its ID. If a theme is merged, GET /themes/{id} for the retired ID returns 410 Gone with code THEME_RETIRED and the final surviving replacement_theme_id. To list current themes together with old merged themes, use GET /themes?include_retired=true; each merged row has the same final pointer, so callers never need to follow a chain. Names, descriptions, theme membership, and aggregate counts can evolve as ClosedLoop AI processes more feedback.

Errors

Non-2xx responses return a JSON body with a user-safe message, a stable code, an optional hint, and any documented error-specific fields. Internal details are never exposed.
/v1 is strict about query parameters: anything an endpoint does not support is rejected with 400 UNSUPPORTED_QUERY_PARAMETER rather than ignored. A filter that silently did nothing would be indistinguishable from one that worked, so a rejection is the only way to tell. Support is PER ENDPOINT — search_mode is valid on /themes and /features and rejected on /coverage.

Rate limits

The public API has no general fixed per-key quota. Semantic search uses implementation-defined protective rate and concurrency safeguards. Clients must handle 429 and honor Retry-After; these safeguards are enforced independently by API replica and are not a fleet-wide numeric quota. /themes and /features share ONE semantic-search budget; they are not separately allowanced. Semantic traffic on either endpoint can therefore produce a 429 on the other.

Timestamps

All timestamps are ISO 8601 in UTC, for example 2026-05-14T10:30:00Z. Date filters (date_from, date_to) accept ISO 8601 dates and are inclusive on both ends. source_date describes when the feedback occurred; updated_at is the incremental export cursor timestamp.

Linking insights and context

Insight and context records carry the same resolved customer_id, so you can line up an insight with the strategic context (churn, competitor, satisfaction) for the same customer. Filter either list by customer_id. New to the API? Start with the API reference introduction.