Create a voice
curl --request POST \
--url https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"name": "<string>",
"description": "<string>",
"clone_method": "<string>",
"model": "<string>",
"language": "<string>",
"labels": {}
}
'import requests
url = "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices"
payload = {
"name": "<string>",
"description": "<string>",
"clone_method": "<string>",
"model": "<string>",
"language": "<string>",
"labels": {}
}
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: '<string>',
description: '<string>',
clone_method: '<string>',
model: '<string>',
language: '<string>',
labels: {}
})
};
fetch('https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => '<string>',
'description' => '<string>',
'clone_method' => '<string>',
'model' => '<string>',
'language' => '<string>',
'labels' => [
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices"
payload := strings.NewReader("{\n \"name\": \"<string>\",\n \"description\": \"<string>\",\n \"clone_method\": \"<string>\",\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"labels\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"<string>\",\n \"description\": \"<string>\",\n \"clone_method\": \"<string>\",\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"labels\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"<string>\",\n \"description\": \"<string>\",\n \"clone_method\": \"<string>\",\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"labels\": {}\n}"
response = http.request(request)
puts response.read_body{
"data": {}
}Endpoints
Create a voice
Create a new (empty) voice in your workspace.
POST
/
flam-voice-svc
/
api
/
v1
/
public
/
voices
Create a voice
curl --request POST \
--url https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"name": "<string>",
"description": "<string>",
"clone_method": "<string>",
"model": "<string>",
"language": "<string>",
"labels": {}
}
'import requests
url = "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices"
payload = {
"name": "<string>",
"description": "<string>",
"clone_method": "<string>",
"model": "<string>",
"language": "<string>",
"labels": {}
}
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: '<string>',
description: '<string>',
clone_method: '<string>',
model: '<string>',
language: '<string>',
labels: {}
})
};
fetch('https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => '<string>',
'description' => '<string>',
'clone_method' => '<string>',
'model' => '<string>',
'language' => '<string>',
'labels' => [
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices"
payload := strings.NewReader("{\n \"name\": \"<string>\",\n \"description\": \"<string>\",\n \"clone_method\": \"<string>\",\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"labels\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"<string>\",\n \"description\": \"<string>\",\n \"clone_method\": \"<string>\",\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"labels\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"<string>\",\n \"description\": \"<string>\",\n \"clone_method\": \"<string>\",\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"labels\": {}\n}"
response = http.request(request)
puts response.read_body{
"data": {}
}Creates a voice in the workspace that owns the API key. A newly created voice has status
draft - it
has no audio yet and cannot synthesize until you enroll a reference clip with
/enroll. To create and enroll in a single request, use
/clone instead.
Creating a voice counts against your plan’s
max_voices limit. Over the cap, the request returns
403 with { "error": "plan limit reached", "feature": "max_voices", "limit": <n> }.Authorization
string
required
Your workspace API key (
flv_…).Body
string
required
Display name for the voice.
string
Optional long-form description.
string
default:"instant"
How the voice will be created. One of
instant, professional, design, or remix. This is
informational metadata on the voice - the value is not currently plan-gated (for example,
professional is accepted on any plan).string
The synthesis model to bind to the voice. Fixed at creation; an unknown value falls back to the
default model.
visibility is not accepted on the public API. New voices are always created as private -
any visibility value sent here is ignored. Publishing a voice is admin-only.string
default:"en"
Primary language (BCP-47, e.g.
en, hi). Used when a synthesis request omits language.object
Optional key/value tags, e.g.
{ "accent": "american", "gender": "female" }.Response
Voice
The created Voice object with status
draft.curl -X POST "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices" \
-H "X-API-Key: $FLAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Studio Narrator","language":"en","labels":{"accent":"american"}}'
const res = await fetch(
`https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices`,
{
method: "POST",
headers: {
"X-API-Key": process.env.FLAM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "Studio Narrator", language: "en", labels: { accent: "american" } }),
},
);
const { data } = await res.json();
console.log(data.id, data.status);
import os, requests
res = requests.post(
f"https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices",
headers={"X-API-Key": os.environ["FLAM_API_KEY"], "Content-Type": "application/json"},
json={"name": "Studio Narrator", "language": "en", "labels": {"accent": "american"}},
)
print(res.json()["data"])
Response
{
"status": 201,
"data": {
"id": "665f1a2b3c4d5e6f7a8b9c0d",
"name": "Studio Narrator",
"status": "draft",
"primary_language": "en",
"visibility": "private",
"clone_method": "instant",
"labels": [{ "key": "accent", "value": "american" }]
},
"error": false
}
Prompt
Copy this into your coding agent (Claude Code, Cursor, Codex…) to implement this endpoint.You are implementing a typed client for one Finesse Voice API endpoint: "Create a voice".
Everything you need is in this prompt. Do not invent anything that is not specified here.
WHAT TO BUILD
- A function createVoice(input) (name it per the project's conventions) that creates a new, empty
voice in the API key's workspace and returns the created Voice.
- Detect the stack first: look for package.json, tsconfig.json, pyproject.toml,
requirements.txt, go.mod, Gemfile, composer.json and similar. Use the project's language, HTTP
client, module layout, error-handling conventions and test runner. If there is no project yet or
the stack is unclear, use TypeScript on Node 18+ with the built-in fetch and no new dependencies.
- If the project already has a Finesse API client module, add this to it (reuse its auth, base
URL, error type and retry helper) instead of creating a parallel one.
- Keep the base URL (https://fi.production.flamapis.com) in one constant or config value.
WHAT THIS ENDPOINT DOES
- It creates a voice with status "draft". A draft voice has no audio and CANNOT synthesize until a
reference clip is enrolled with POST /flam-voice-svc/api/v1/public/voices/{id}/enroll (or you use the one-shot
POST /flam-voice-svc/api/v1/public/voices/clone instead).
ENDPOINT
- Method: POST
- URL: https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices
- Content-Type: application/json
REQUEST BODY
- name (string, required): display name. Validate it is non-empty locally.
- description (string, optional): long-form description.
- clone_method (string, optional, default "instant"): one of "instant", "professional", "design",
"remix". Informational metadata only; not plan-gated.
- model (string, optional): synthesis model to bind to the voice. Fixed at creation; an unknown
value falls back to the default model.
- language (string, optional, default "en"): BCP-47 primary language (e.g. "en", "hi"). Becomes
the voice's primary_language, used when a synthesis request omits language.
- labels (object, optional): flat string-to-string map, e.g. { "accent": "american" }.
Note the asymmetry: you SEND labels as an object, but the response returns them as an array of
{ key, value } pairs.
- Do NOT send visibility. It is not accepted on the public API and is ignored; new voices are
always "private". Leave it out of the input type.
- Omit optional fields that the caller did not provide.
{
"name": "Studio Narrator",
"language": "en",
"labels": { "accent": "american" }
}
AUTHENTICATION
- Send the header X-API-Key: <workspace API key> on every request. Keys start with "flv_".
- Read the key from the environment variable FLAM_API_KEY (or the project's existing secrets or
config mechanism). Fail fast with a clear error if it is missing, before making any request.
- Never hard-code, log or commit the key, and never ship it in browser/client-side code: call this
from server-side code only. The key can synthesize with every voice in its workspace.
- A key is bound to one workspace. Referencing a voice or job from another workspace returns 404.
RESPONSE ENVELOPE
JSON responses use one envelope. On success "error" is false and the payload is in "data":
{
"status": 200,
"data": { "...": "endpoint payload" },
"error": false
}
On failure "error" is true, "data" is omitted and "message" carries the detail:
{
"status": 404,
"error": true,
"message": "Voice not found"
}
Billing and plan denials do NOT use the envelope. They return a flat JSON body whose "error"
field is a string, not a boolean:
{ "error": "plan limit reached", "feature": "max_voices", "limit": 10 }
{ "error": "quota_exhausted", "unit": "character", "limit": 100000, "used": 100000 }
The "error" string is the denial reason (for example quota_exhausted, insufficient_credits,
limit_reached, feature_disabled, or one of the messages above). Tell the two shapes apart by
checking whether "error" is a boolean (envelope) or a string (flat denial), and expose
feature / unit / limit / used on your error type when present.
SUCCESS RESPONSE (201, application/json)
"data" is the created Voice, with status "draft" and visibility "private":
{
"status": 201,
"data": {
"id": "665f1a2b3c4d5e6f7a8b9c0d",
"name": "Studio Narrator",
"status": "draft",
"primary_language": "en",
"visibility": "private",
"clone_method": "instant",
"labels": [{ "key": "accent", "value": "american" }]
},
"error": false
}
VOICE OBJECT (define a type for it; timestamps are Unix epoch MILLISECONDS)
- id: string. Unique voice id, 24-character hex. Used by the synthesize endpoints.
- name: string. Display name.
- description: string, optional.
- status: string. Only "ready" voices can synthesize. A voice being created reports "processing",
"failed" if creation did not succeed, and a newly created voice with no audio yet is "draft".
Type it as a string union of these values but tolerate other strings (treat them as not usable).
- clone_method: "instant" | "professional" | "design" | "remix".
- visibility: "private" | "org" | "public".
- primary_language: string, BCP-47 (e.g. "en"). Used when a synthesis request omits language.
- labels: array of { key: string, value: string }, optional. Common keys: accent, gender, age,
use_case. Example: [{ "key": "accent", "value": "american" }]
- settings: object, optional, present only when configured. Synthesis defaults: stability,
similarity, style, speed, pitch, volume, use_speaker_boost, emotion, engine_params.
- sample_audio_url: string, optional. Public URL of a short preview clip.
- base_voice_id: string, optional. Present when derived from another voice (clone_method "remix").
- created_at, updated_at: integers (epoch ms).
Optional fields may be absent. Responses may contain extra fields: ignore them, never fail on them.
Example:
{
"id": "665f1a2b3c4d5e6f7a8b9c0d",
"name": "Studio Narrator",
"description": "Warm, neutral narration voice.",
"status": "ready",
"clone_method": "instant",
"visibility": "public",
"primary_language": "en",
"labels": [
{ "key": "accent", "value": "american" },
{ "key": "gender", "value": "female" }
],
"sample_audio_url": "https://storage.googleapis.com/.../sample.wav",
"created_at": 1717939200000,
"updated_at": 1717939260000
}
LIMITS AND BILLING
- Creating a voice counts against the plan's max_voices limit. Over the cap the request returns
403 with the flat body { "error": "plan limit reached", "feature": "max_voices", "limit": <n> }.
Surface it as a plan-limit error that includes the limit. Deleting a voice frees a slot.
ERRORS
- 400: invalid request, e.g. missing name or an invalid clone_method. Do not retry.
- 401: X-API-Key missing, malformed, invalid or revoked. Do not retry.
- 403: plan limit reached (flat body above). Do not retry.
- 429: rate limited. Retry with backoff.
- Surface failures as one typed error (for example FinesseApiError) carrying: HTTP status, message
(the envelope "message" or the flat "error" string), optional feature / unit / limit / used, a
retryable flag, and the raw body for debugging.
- Parse error bodies defensively: if a body is not valid JSON, keep the raw text in the error.
RETRIES
- This POST creates a resource, so do NOT retry it automatically after a timeout or 5xx: the first
attempt may have succeeded and a retry could create a duplicate. Surface the error instead.
- A 429 (rate limited) means the request was refused, so retrying it with exponential backoff plus
jitter (about 1s, 2s, 4s; at most 3 retries) is safe.
- Never retry 400, 401, 402, 403, 404 or 422.
ACCEPTANCE CRITERIA
- createVoice sends POST with a JSON body containing only the supported fields, never visibility.
- It returns the typed Voice from "data" on 201.
- The max_voices 403 is exposed as a distinct error with its limit; the flat body is parsed
correctly even though it has no envelope.
- A timeout or 5xx is not retried automatically.
- A missing FLAM_API_KEY fails before any network call. The key is never logged.
TEST PLAN
- Unit tests with mocked HTTP: 201 returns a draft Voice; the body contains no visibility key and
no undefined optional keys; 400 envelope; 403 plan-limit flat body; 429 then 201 (retry); a
502 is NOT retried.
- Smoke script: create a voice named "smoke-test-<timestamp>", print its id and status (expect
"draft"), then clean up with DELETE https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/{id} (same X-API-Key header; success
is { "data": { "deleted": true } }) so the test does not consume a max_voices slot.
RULES
- Implement only what this spec describes. Do not invent parameters, fields, headers, endpoints or
error codes. If something is ambiguous, missing, or conflicts with the existing codebase, stop
and ask me before guessing.
- Match the project's existing style and conventions; do not refactor unrelated code.

