> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://onepin.ai/docs/reference/api-reference/voices/get-voice-facets-api-v-1-voices-facets-get/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://onepin.ai/_mcp/server. # Get Voice Facets GET https://api.onepin.ai/api/v1/voices/facets Filter-bar options (chips) for the voice browser, one list per dimension. Returns `providers`, `models`, `languages` (data-driven) plus `genders`, `ages`, `categories`, `accents` (fixed enums) as `VoiceFacetItem[]` so the FE builds the whole filter bar — with per-chip count badges — in a single request instead of hardcoding option lists (mirrors `GET /dictionary/languages`). Each item is `{value, label, count}`: `value` is passed straight back to `GET /voices`; `label` is the display name for providers/models and `null` elsewhere (the FE owns language + enum labels); `count` is the number of matching voices. A language surfaces as one chip keyed by its canonical allowlist locale: every declared locale is folded onto the locale the `GET /voices` filter would match it against (bare `ko` and regioned `ko-kr` both count under `ko-kr`), so variants never split into duplicate chips for the identical filter. Model counts include only voices that explicitly declare the model. Language counts include voices that declare the exact regional locale plus voices that declare its bare family (`en` contributes to every supported `en-*` locale). A voice with no declared `supported_models` is "general use" — `GET /voices` matches it against every model filter but no `models` chip counts it, so a model chip's count can be lower than the `GET /voices?model=` result. Languages have no such gap: no-locale voices are excluded from both the language chips and `GET /voices?language=`, so language chip counts match the row counts. Accepts the SAME filters as `GET /voices` (tab scope `source`/`favorites_only`, plus `provider`/`model`/`language`/`gender`/`age`/`category`/`accent`/`search`). `count` is context-aware (faceted search): each dimension's counts apply every OTHER active filter but exclude that dimension's own selection — e.g. with `provider=elevenlabs` the language counts are scoped to ElevenLabs, while the provider chips still show every provider so the caller can switch. `search` follows the SAME two modes as `GET /voices` (see `_semantic_search_active`). In SEMANTIC mode (flag on, embedder available, platform in scope) the chips are counted over the population the semantic list can return — every active filter, plus "carries a description embedding OR is in the current ranked set" (the ANN only ranks embedded voices; the keyword arm contributes the rest) — so the chips describe the voices the list actually shows and never collapse to "No matches" on a query that has no literal name/tag hit. Per-dimension self-exclusion applies in full, exactly as in lexical mode. Counts are clamped to `VOICE_SEARCH_MAX_RANKED`, because the semantic list's `pagination.total` is that same capped ranked-set size — so a chip's `count` is the number of rows `GET /voices` returns once that value is selected. Two bounded exceptions: a voice reachable only through the keyword arm is counted only under the value already selected (the ranked set is computed under the current filters), and ANN recall can return fewer rows than the chip promises. In the LEXICAL fallback (flag off / no embedder / non-platform source / embed fault) `search` is the `name`/`descriptor`/`tags` ILIKE and counts are exact (no cap), exactly as before. Chips are drawn from the same population `GET /voices` returns, so the official-locale restriction applies here too and no chip can open an empty page. For the `model` dimension the restriction is evaluated per capability row rather than per voice — a voice whose only official locale sits on a sibling model does not count toward this model's chip, because `GET /voices?model=` would not return it either. Count-0 policy: data-driven dimensions omit count-0 values (only present ones, each a valid `GET /voices` filter — providers/models restricted to the enabled catalog, languages to the supported-locale allowlist, so a chip never 422s). Enum dimensions always return the full enum in natural order, count-0 included, for the FE to grey out. Reference: https://onepin.ai/docs/reference/api-reference/voices/get-voice-facets-api-v-1-voices-facets-get ## Authentication - `Authorization` header (bearer token, required) — Clerk JWT token - `Authorization` header (bearer token, required) — Onepin live API key (`op_live_...`). Test and public keys are reserved in Phase 1. ## Servers - `https://api.onepin.ai` (prod, default) - `https://dev-api.onepin.ai` (dev) ## Request ### Query parameters - `favorites_only` (boolean, optional, default: false) — Favorites tab scope - `source` (list of enum, optional, nullable) — Tab scope — repeat for OR, same values as GET /voices (e.g. platform, workspace) - Allowed values: `platform`, `recorded`, `uploaded`, `workspace` - `gender` (list of enum, optional, nullable) — Repeat for OR - Allowed values: `male`, `female`, `neutral` - `age` (list of enum, optional, nullable) — Repeat for OR - Allowed values: `young`, `middle_aged`, `old` - `category` (list of enum, optional, nullable) — Repeat for OR - Allowed values: `news`, `narration`, `story`, `podcast`, `conversational`, `social_media`, `educational`, `business` - `accent` (list of enum, optional, nullable) — Repeat for OR - Allowed values: `american`, `british`, `australian`, `indian`, `canadian`, `irish`, `african`, `european`, `east_asian`, `southeast_asian`, `latin_american`, `middle_eastern`, `slavic`, `spanish`, `french`, `german`, `italian`, `portuguese`, `japanese`, `korean`, `chinese`, `thai`, `vietnamese`, `other` - `search` (string, optional, nullable) - `provider` (list of string, optional, nullable) — Repeat for OR, e.g. ?provider=elevenlabs&provider=rime - `model` (list of string, optional, nullable) — Repeat for OR. Filters platform voices by TTS model, e.g. ?model=arcana&model=sonic-2 - `language` (list of string, optional, nullable) — Repeat for OR, e.g. ?language=en-us&language=ko-kr ### Headers - `X-Workspace-Id` (string, optional, nullable) ## Response ### 200 Successful Response - `data` (VoiceFacetsOut, required) — Filter options for the voice browser, one ``VoiceFacetItem[]`` per chip. Two families of dimension: * **Data-driven** — ``providers``, ``models``, ``languages``: only values with a scoped voice count are returned (count is always ≥ 1; count-0 values are omitted). A language value may be derived from a bare family tag on a scoped voice. Every value is guaranteed to be a valid ``GET /voices`` filter (provider/model restricted to the enabled catalog, language to the supported-locale allowlist), so selecting one never yields a 422 or empty page. Sorted count DESC, then value ASC. * **Enum** — ``genders``, ``ages``, ``categories``, ``accents``: the FULL fixed enum is always returned in natural enum order, including count-0 values (the FE greys those out). ``label`` is ``None`` (the FE owns enum labels). ``count`` is context-aware (faceted search): each dimension's counts apply every OTHER active filter but exclude that dimension's own selection, so a chip's number reflects "results if I also pick this" without the dimension suppressing its own alternatives. - `meta` (Meta, required) ## Errors ### 422 Unprocessable Entity Error Validation Error - `detail` (list of ValidationError, optional) ## Types ### VoiceFacetsOut Filter options for the voice browser, one ``VoiceFacetItem[]`` per chip. Two families of dimension: * **Data-driven** — ``providers``, ``models``, ``languages``: only values with a scoped voice count are returned (count is always ≥ 1; count-0 values are omitted). A language value may be derived from a bare family tag on a scoped voice. Every value is guaranteed to be a valid ``GET /voices`` filter (provider/model restricted to the enabled catalog, language to the supported-locale allowlist), so selecting one never yields a 422 or empty page. Sorted count DESC, then value ASC. * **Enum** — ``genders``, ``ages``, ``categories``, ``accents``: the FULL fixed enum is always returned in natural enum order, including count-0 values (the FE greys those out). ``label`` is ``None`` (the FE owns enum labels). ``count`` is context-aware (faceted search): each dimension's counts apply every OTHER active filter but exclude that dimension's own selection, so a chip's number reflects "results if I also pick this" without the dimension suppressing its own alternatives. - `providers` (list of VoiceFacetItem, required) - `models` (list of VoiceFacetItem, required) - `languages` (list of VoiceFacetItem, required) — Region-qualified lowercase BCP-47 language filter options. Bare language values are never emitted; a bare family tag on a voice contributes to every supported regional sibling's count. - `genders` (list of VoiceFacetItem, required) - `ages` (list of VoiceFacetItem, required) - `categories` (list of VoiceFacetItem, required) - `accents` (list of VoiceFacetItem, required) ### Meta - `request_id` (string, required) - `timestamp` (datetime, required) ### ValidationError - `loc` (list of ValidationErrorLocItems, required) - `msg` (string, required) - `type` (string, required) - `input` (any, optional) - `ctx` (ValidationErrorCtx, optional) ### VoiceFacetItem One selectable filter option (chip) for the voice browser filter bar. `value` is the exact token the caller passes back to `GET /voices` (a provider/model key, a lowercase BCP-47 locale, or an enum value like `female`). `label` is the display name for provider/model facets and `None` for languages and enum dimensions — the FE holds those labels (it renders locale flags/names from the code via `Intl`). `count` is the number of voices matching `value` under the current request context (tab/workspace scope + every OTHER active filter — see `VoiceFacetsOut`). For `models`, `count` includes only voices that explicitly declare `value` in `supported_models`. For `languages`, it includes voices that declare the exact regional locale plus voices that declare its bare family (for example, `en` contributes to every supported `en-*` locale). A voice with no declared `supported_models` is "general use" — `GET /voices` matches it against every `model` filter, yet it is counted in no `models` chip, so a `models` chip's `count` can be lower than the row count `GET /voices?model=` actually returns. `languages` has no such gap: a voice with no declared locales is excluded from both the language chips AND `GET /voices?language=`, so language chip counts match the filtered row counts. - `value` (string, required) - `count` (integer, required) - `label` (string, optional, nullable) ### ValidationErrorLocItems ### ValidationErrorCtx ## Examples **Response** ```json { "data": { "providers": [ { "value": "string", "count": 1, "label": "string" } ], "models": [ { "value": "string", "count": 1, "label": "string" } ], "languages": [ { "value": "string", "count": 1, "label": "string" } ], "genders": [ { "value": "string", "count": 1, "label": "string" } ], "ages": [ { "value": "string", "count": 1, "label": "string" } ], "categories": [ { "value": "string", "count": 1, "label": "string" } ], "accents": [ { "value": "string", "count": 1, "label": "string" } ] }, "meta": { "request_id": "string", "timestamp": "2024-01-15T09:30:00Z" } } ``` **SDK Code** ```python import requests url = "https://api.onepin.ai/api/v1/voices/facets" headers = {"Authorization": "Bearer "} response = requests.get(url, headers=headers) print(response.json()) ``` ```javascript const url = 'https://api.onepin.ai/api/v1/voices/facets'; const options = {method: 'GET', headers: {Authorization: 'Bearer '}}; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ```go package main import ( "fmt" "net/http" "io" ) func main() { url := "https://api.onepin.ai/api/v1/voices/facets" req, _ := http.NewRequest("GET", url, nil) req.Header.Add("Authorization", "Bearer ") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(res) fmt.Println(string(body)) } ``` ```ruby require 'uri' require 'net/http' url = URI("https://api.onepin.ai/api/v1/voices/facets") http = Net::HTTP.new(url.host, url.port) http.use_ssl = true request = Net::HTTP::Get.new(url) request["Authorization"] = 'Bearer ' response = http.request(request) puts response.read_body ``` ```java import com.mashape.unirest.http.HttpResponse; import com.mashape.unirest.http.Unirest; HttpResponse response = Unirest.get("https://api.onepin.ai/api/v1/voices/facets") .header("Authorization", "Bearer ") .asString(); ``` ```php request('GET', 'https://api.onepin.ai/api/v1/voices/facets', [ 'headers' => [ 'Authorization' => 'Bearer ', ], ]); echo $response->getBody(); ``` ```csharp using RestSharp; var client = new RestClient("https://api.onepin.ai/api/v1/voices/facets"); var request = new RestRequest(Method.GET); request.AddHeader("Authorization", "Bearer "); IRestResponse response = client.Execute(request); ``` ```swift import Foundation let headers = ["Authorization": "Bearer "] let request = NSMutableURLRequest(url: NSURL(string: "https://api.onepin.ai/api/v1/voices/facets")! as URL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0) request.httpMethod = "GET" request.allHTTPHeaderFields = headers let session = URLSession.shared let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in if (error != nil) { print(error as Any) } else { let httpResponse = response as? HTTPURLResponse print(httpResponse) } }) dataTask.resume() ``` > Onepin is a voice workflow platform that orchestrates, validates, and ships production-ready audio across 33 TTS models.