> 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.

# Favorite Voice

POST https://api.onepin.ai/api/v1/voices/{voice_id}/favorite

Add a voice to the current workspace's favorites.

The response is a full `VoiceOut` and is narrowed exactly as `GET /voices/{voice_id}`
is, so favoriting a voice never reveals locales the read endpoints hide. Note that
favoriting a platform voice with no official locale succeeds but that voice will
not appear under `?favorites_only=true`, which applies the same list restriction.

Favorites are workspace-scoped, not per-user: all members of the workspace
see the same favorited set. Idempotent — favoriting a voice that is already
favorited succeeds without error. Returns the voice with `is_favorite=true`.
Requires the caller to have at least editor role in the workspace. Returns
404 for a platform voice from a provider that is not available to your
account; voices your workspace owns can always be favorited.

Reference: https://onepin.ai/docs/reference/api-reference/voices/favorite-voice-api-v-1-voices-voice-id-favorite-post

## 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

### Path parameters

- `voice_id` (string, required)

### Headers

- `X-Workspace-Id` (string, optional, nullable)

## Response

### 200

Successful Response

- `data` (VoiceOut, required)
- `meta` (Meta, required)

## Errors

### 422 Unprocessable Entity Error

Validation Error

- `detail` (list of ValidationError, optional)

## Types

### VoiceOut

- `id` (string, required) — Unique voice identifier.
- `name` (string, required) — Display name of the voice.
- `provider` (string, required) — Speech synthesis provider code for this voice.
- `provider_voice_id` (string, required) — Provider-assigned voice identifier used when submitting synthesis requests.
- `is_active` (boolean, required) — Whether the voice is available for use. Inactive voices are not returned by list or synthesis endpoints.
- `created_at` (datetime, required) — When the voice was added to the platform or workspace.
- `updated_at` (datetime, required) — When the voice record was last updated.
- `description` (string, optional, nullable) — Human-readable description of the voice's character and style.
- `gender` (enum, optional, nullable) — Perceived gender presentation of the voice.
  - Allowed values: `male`, `female`, `neutral`
- `accent` (enum, optional, nullable) — Accent of the voice, if classified.
  - 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`
- `age` (enum, optional, nullable) — Perceived age range of the voice, if classified.
  - Allowed values: `young`, `middle_aged`, `old`
- `category` (enum, optional, nullable) — Intended use-case category (e.g. narration, conversational).
  - Allowed values: `news`, `narration`, `story`, `podcast`, `conversational`, `social_media`, `educational`, `business`
- `color` (string, optional, nullable) — Brand color associated with the voice in the UI, as a hex string.
- `tags` (list of string, optional, nullable) — Freeform keyword tags for filtering and search.
- `uses_count` (integer, optional, nullable) — Number of times this voice has been used in workflow runs across the platform.
- `user_id` (string, optional, nullable) — Owner user ID for workspace-cloned or user-created voices. Null for platform voices.
- `source` (enum, optional) — Origin of the voice: `platform` for system-provided voices, `recorded`/`uploaded` for voices added or cloned by the workspace, `provider_imported` for voices imported from the workspace's own provider account.
  - Allowed values: `platform`, `recorded`, `uploaded`, `provider_imported`
- `availability` (enum, optional, nullable) — Whether an imported voice can currently be selected. `unavailable` rows stay listed and readable but are rejected by workflow save, run, and synthesis. Null for platform and legacy workspace voices.
  - Allowed values: `available`, `unavailable`
- `unavailable_reason` (enum, optional, nullable) — Why an imported voice is unavailable. Null when `availability` is `available` or absent.
  - Allowed values: `available`, `reconciliation_pending`, `provider_key_unavailable`, `provider_voice_missing`, `provider_import_unavailable`
- `duration_seconds` (double, optional, nullable) — Duration of the audio sample in seconds, if available.
- `sample_url` (string, optional, nullable) — Time-limited presigned URL for the audio preview sample. For a voice that declares no English locale this is one of its `preview_locales` clips when any exists, so the sample is never in a language the voice cannot speak; otherwise it is the voice's default clip, whose spoken language is not guaranteed. It does not vary with the `language` filter — read `language_sample_url` (same response, no extra request) or GET /voices/\{voice\_id}/preview for that. Valid for 1 hour; regenerate by fetching the voice again.
- `language_sample_url` (string, optional, nullable) — Time-limited presigned URL for the preview clip in the requested `language`, and the one field on this model that DOES follow that filter. Populated only when the request carried `language=`; null without it, and null when the voice has no preview clip in a locale that filter expands to. It is the same row GET /voices/\{voice\_id}/preview?language= would serve, resolved through the same repository method, so a caller that plays this hears exactly what that endpoint would return — without a request per voice. Read `language_sample_locale` for the region actually served. Valid for 1 hour; regenerate by fetching the list again.
- `language_sample_locale` (string, optional, nullable) — The actual regioned locale `language_sample_url` was served from, never a reflection of the request. A bare-family filter like `?language=en` expands to both `en-us` and `en-gb` and which one wins is deterministic but arbitrary, so label the clip from this and not from what you asked for. Null exactly when `language_sample_url` is.
- `supported_languages` (list of string, optional, nullable) — BCP-47 language codes this voice supports, restricted to the officially supported locales. Null means the voice declares no locales at all; it is not matched by any `language` filter — a voice must positively declare a locale to surface under that filter. An empty array is a different state: the voice declared locales, but none of them is officially supported. Platform voices in that state are absent from the list endpoints entirely, so an empty array is only seen on a single-voice read. Voices your workspace owns are exempt from the restriction and report every locale they declare.
- `preview_locales` (list of string, optional) — Locales this voice has ready-to-play preview audio for. Fetch it with GET /voices/\{voice\_id}/preview?language=\<locale>. This is what the voice can be HEARD in, not what it can SPEAK — see supported\_languages for that. Empty means no locale preview exists yet.
- `supported_models` (list of string, optional, nullable) — Deprecated compatibility union of model identifiers this voice is compatible with. Null means compatible with all available models for the provider.
- `model_capabilities` (list of VoiceModelCapabilityOut, optional) — Authoritative model-to-language observations after a successful full provider sync. Empty means no authoritative pair observation is available yet.
- `is_favorite` (boolean, optional, default: false) — Whether this voice is in the current workspace's favorites list.

### 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)

### VoiceModelCapabilityOut

- `model` (string, required) — Model identifier for this exact voice capability.
- `languages_known` (boolean, required) — Whether the provider authoritatively supplied locale metadata. False means unknown; true with an empty supported_languages array means known-empty. On app-facing responses an empty array can ALSO mean the provider declared locales for this model but none of them is officially supported — the voice was kept because a sibling model carries an official locale. Use the admin API to see the raw declarations.
- `supported_languages` (list of string, optional)

### ValidationErrorLocItems

### ValidationErrorCtx

## Examples

**Response**

```json
{
  "data": {
    "id": "string",
    "name": "string",
    "provider": "string",
    "provider_voice_id": "string",
    "is_active": true,
    "created_at": "2024-01-15T09:30:00Z",
    "updated_at": "2024-01-15T09:30:00Z",
    "description": "string",
    "gender": "male",
    "accent": "american",
    "age": "young",
    "category": "news",
    "color": "string",
    "tags": [
      "string"
    ],
    "uses_count": 1,
    "user_id": "string",
    "source": "platform",
    "availability": "available",
    "unavailable_reason": "available",
    "duration_seconds": 1.1,
    "sample_url": "string",
    "language_sample_url": "string",
    "language_sample_locale": "string",
    "supported_languages": [
      "string"
    ],
    "preview_locales": [
      "string"
    ],
    "supported_models": [
      "string"
    ],
    "model_capabilities": [
      {
        "model": "string",
        "languages_known": true,
        "supported_languages": [
          "string"
        ]
      }
    ],
    "is_favorite": false
  },
  "meta": {
    "request_id": "string",
    "timestamp": "2024-01-15T09:30:00Z"
  }
}
```

**SDK Code**

```python
import requests

url = "https://api.onepin.ai/api/v1/voices/voice_id/favorite"

headers = {"Authorization": "Bearer <token>"}

response = requests.post(url, headers=headers)

print(response.json())
```

```javascript
const url = 'https://api.onepin.ai/api/v1/voices/voice_id/favorite';
const options = {method: 'POST', 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/voice_id/favorite"

	req, _ := http.NewRequest("POST", 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/voice_id/favorite")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.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.post("https://api.onepin.ai/api/v1/voices/voice_id/favorite")
  .header("Authorization", "Bearer <token>")
  .asString();
```

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

$client = new \GuzzleHttp\Client();

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

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.onepin.ai/api/v1/voices/voice_id/favorite");
var request = new RestRequest(Method.POST);
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/voice_id/favorite")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
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()
```