> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://onepin.ai/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://onepin.ai/docs/_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=<value>` actually returns. `languages` has no such gap: a voice with no declared locales is excluded from both the language chips AND `GET /voices?language=<value>`, 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 <token>"}

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 <token>'}};

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 <token>")

	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 <token>'

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.get("https://api.onepin.ai/api/v1/voices/facets")
  .header("Authorization", "Bearer <token>")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('GET', 'https://api.onepin.ai/api/v1/voices/facets', [
  'headers' => [
    'Authorization' => 'Bearer <token>',
  ],
]);

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 <token>");
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = ["Authorization": "Bearer <token>"]

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()
```