Synthesize speech
curl --request POST \
--url https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/{id}/speak \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"text": "<string>",
"language": "<string>",
"output_format": "<string>",
"sample_rate": 123,
"num_step": 123,
"expressiveness": 123
}
'import requests
url = "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/{id}/speak"
payload = {
"text": "<string>",
"language": "<string>",
"output_format": "<string>",
"sample_rate": 123,
"num_step": 123,
"expressiveness": 123
}
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({
text: '<string>',
language: '<string>',
output_format: '<string>',
sample_rate: 123,
num_step: 123,
expressiveness: 123
})
};
fetch('https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/{id}/speak', 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/{id}/speak",
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([
'text' => '<string>',
'language' => '<string>',
'output_format' => '<string>',
'sample_rate' => 123,
'num_step' => 123,
'expressiveness' => 123
]),
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/{id}/speak"
payload := strings.NewReader("{\n \"text\": \"<string>\",\n \"language\": \"<string>\",\n \"output_format\": \"<string>\",\n \"sample_rate\": 123,\n \"num_step\": 123,\n \"expressiveness\": 123\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/{id}/speak")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"text\": \"<string>\",\n \"language\": \"<string>\",\n \"output_format\": \"<string>\",\n \"sample_rate\": 123,\n \"num_step\": 123,\n \"expressiveness\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/{id}/speak")
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 \"text\": \"<string>\",\n \"language\": \"<string>\",\n \"output_format\": \"<string>\",\n \"sample_rate\": 123,\n \"num_step\": 123,\n \"expressiveness\": 123\n}"
response = http.request(request)
puts response.read_body{
"Content-Type": "<string>",
"X-Sample-Rate": "<string>",
"X-Audio-URL": "<string>",
"X-Billing-Remaining": "<string>"
}Endpoints
Synthesize speech
Turn text into a single audio file using a cloned voice.
POST
/
flam-voice-svc
/
api
/
v1
/
public
/
voices
/
{id}
/
speak
Synthesize speech
curl --request POST \
--url https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/{id}/speak \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"text": "<string>",
"language": "<string>",
"output_format": "<string>",
"sample_rate": 123,
"num_step": 123,
"expressiveness": 123
}
'import requests
url = "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/{id}/speak"
payload = {
"text": "<string>",
"language": "<string>",
"output_format": "<string>",
"sample_rate": 123,
"num_step": 123,
"expressiveness": 123
}
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({
text: '<string>',
language: '<string>',
output_format: '<string>',
sample_rate: 123,
num_step: 123,
expressiveness: 123
})
};
fetch('https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/{id}/speak', 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/{id}/speak",
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([
'text' => '<string>',
'language' => '<string>',
'output_format' => '<string>',
'sample_rate' => 123,
'num_step' => 123,
'expressiveness' => 123
]),
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/{id}/speak"
payload := strings.NewReader("{\n \"text\": \"<string>\",\n \"language\": \"<string>\",\n \"output_format\": \"<string>\",\n \"sample_rate\": 123,\n \"num_step\": 123,\n \"expressiveness\": 123\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/{id}/speak")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"text\": \"<string>\",\n \"language\": \"<string>\",\n \"output_format\": \"<string>\",\n \"sample_rate\": 123,\n \"num_step\": 123,\n \"expressiveness\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/{id}/speak")
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 \"text\": \"<string>\",\n \"language\": \"<string>\",\n \"output_format\": \"<string>\",\n \"sample_rate\": 123,\n \"num_step\": 123,\n \"expressiveness\": 123\n}"
response = http.request(request)
puts response.read_body{
"Content-Type": "<string>",
"X-Sample-Rate": "<string>",
"X-Audio-URL": "<string>",
"X-Billing-Remaining": "<string>"
}Synthesizes
See Synthesize parameters for the full field reference.
text with the given voice and returns the raw audio bytes in the response body. The
voice must be enrolled (status ready).
Unlike the JSON endpoints, a successful response is the audio file itself - not a JSON envelope. Errors
still return the standard envelope.
Authorization
string
required
Your workspace API key (
flv_…).Path parameters
string
required
The voice id.
Body
string
required
The text to synthesize. Maximum 10,000 characters. Billed per character against your plan’s
monthly allowance; repeating an identical request (same voice, text, and parameters) is served
from cache and not billed again.
string
BCP-47 language code (for example
en, hi). Defaults to the voice’s primary language.string
default:"wav"
Output encoding. One of
wav, s16le, s24le, s32le, f32le, f64le. wav is a self-contained
file; the s*/f* formats are raw, headerless PCM.integer
default:24000
Output sample rate in Hz. Defaults to the provider’s native rate (
24000); values above it are
upsampled.integer
default:32
Number of inference steps,
1–32 (a quality/latency trade-off). Provider default (32) if
omitted.number
Style/expressiveness control,
0.0–1.0. Provider default if omitted.Response headers
string
The audio MIME type, e.g.
audio/wav for output_format: wav.string
The sample rate of the returned audio, in Hz (when reported by the provider).
string
A public URL to the stored audio file, when object storage is enabled. Useful for download/share.
string
Your remaining character allowance for the month (present when billing is active). An exhausted
allowance returns
402 when enforcement is on - see Errors.curl -X POST \
"https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/665f1a2b3c4d5e6f7a8b9c0d/speak" \
-H "X-API-Key: $FLAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"Hello from Finesse.","language":"en","output_format":"wav"}' \
--output speech.wav
import { writeFile } from "node:fs/promises";
const id = "665f1a2b3c4d5e6f7a8b9c0d";
const res = await fetch(
`https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/${id}/speak`,
{
method: "POST",
headers: {
"X-API-Key": process.env.FLAM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ text: "Hello from Finesse.", language: "en", output_format: "wav" }),
},
);
if (!res.ok) throw new Error(`Synthesis failed: ${res.status}`);
await writeFile("speech.wav", Buffer.from(await res.arrayBuffer()));
import os, requests
voice_id = "665f1a2b3c4d5e6f7a8b9c0d"
res = requests.post(
f"https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/{voice_id}/speak",
headers={"X-API-Key": os.environ["FLAM_API_KEY"], "Content-Type": "application/json"},
json={"text": "Hello from Finesse.", "language": "en", "output_format": "wav"},
)
res.raise_for_status()
with open("speech.wav", "wb") as f:
f.write(res.content)
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: "Synthesize speech".
Everything you need is in this prompt. Do not invent anything that is not specified here.
WHAT TO BUILD
- A function synthesizeSpeech(voiceId, params) (name it per the project's conventions) that turns
text into ONE audio file with the given voice and returns:
{ audio: bytes, contentType: string, sampleRate?: number, audioUrl?: string,
billingRemaining?: number }
Use the stack's natural byte type (Buffer/Uint8Array, bytes, []byte).
- Optionally a thin wrapper that writes the audio to a file path, if the project needs it.
- 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.
ENDPOINT
- Method: POST
- URL: https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/{id}/speak
- Request Content-Type: application/json
- The voice must be enrolled (status "ready"), and must belong to the key's workspace.
PARAMETERS
- id (path, string, required): the voice id, a 24-character hex string (for example
"665f1a2b3c4d5e6f7a8b9c0d"). Validate it locally against /^[0-9a-f]{24}$/i and URL-encode it.
REQUEST BODY
- text (string, required): the text to synthesize. Maximum 10,000 characters; over the limit
returns 400 "text too long (max 10000 chars)". Check locally: reject empty text and count
characters (Unicode code points, not bytes), e.g. [...text].length in JavaScript.
- language (string, optional): BCP-47 code (e.g. "en", "hi", "es"). Defaults to the voice's
primary_language.
- output_format (string, optional, default "wav"): one of
wav - self-contained WAV file (recommended for playback/download)
s16le - signed 16-bit little-endian PCM (headerless)
s24le - signed 24-bit little-endian PCM (headerless)
s32le - signed 32-bit little-endian PCM (headerless)
f32le - 32-bit float little-endian PCM (headerless)
f64le - 64-bit float little-endian PCM (headerless)
The headerless PCM formats need a known sample rate and channel count to play.
- sample_rate (integer, optional, default 24000): output rate in Hz. Defaults to the provider's
native rate (24000); values above it are upsampled.
- num_step (integer, optional, default 32): inference steps, 1-32; a quality vs latency trade-off.
- expressiveness (number, optional): style/expressiveness control, 0.0-1.0. Provider default when
omitted.
- Validate the enums and ranges locally and omit optional fields the caller did not set.
{
"text": "The quick brown fox jumps over the lazy dog.",
"language": "en",
"output_format": "wav",
"sample_rate": 24000,
"expressiveness": 0.5
}
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.
SUCCESS RESPONSE (200): RAW AUDIO BYTES, NOT JSON
- The body is the audio file itself. Do not parse it as JSON; read it as binary
(e.g. await res.arrayBuffer()).
- Check the status BEFORE reading the body: only a 2xx body is audio.
- Response headers to capture:
Content-Type the audio MIME type, e.g. audio/wav for output_format "wav"
X-Sample-Rate sample rate of the returned audio in Hz (when reported); parse to a number
X-Audio-URL public URL of the stored audio file, when object storage is enabled
X-Billing-Remaining remaining monthly character allowance, when billing is active
All of these except Content-Type may be absent: type them as optional.
ERROR RESPONSES (JSON, even though success is binary)
Errors use the standard envelope:
{
"status": 400,
"error": true,
"message": "Voice is not enrolled yet"
}
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.
- 400: invalid request: missing or invalid field, text over 10,000 characters, or the voice is not
ready yet ("Voice is not enrolled yet"). Do not retry.
- 401: X-API-Key missing, malformed, invalid or revoked. Do not retry.
- 402: monthly character allowance exhausted (flat body, e.g. quota_exhausted with unit, limit,
used). Do not retry; surface it so the app can prompt a top-up/upgrade.
- 403: not permitted, plan limit reached, or feature disabled for the plan (flat body). Do not
retry.
- 404: voice not found, including voices owned by another workspace. Do not retry.
- 422: the voice provider rejected the input. Do not retry.
- 429: rate limited. Retry with backoff.
- 502: the voice provider is warming up or temporarily unavailable. 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 AND TIMEOUTS
- Retry 429, 502 and network errors/timeouts with exponential backoff plus jitter (about 1s, 2s,
4s; at most 3 retries). Retrying is safe here: an identical request (same voice, text and
parameters) is served from cache and not billed again.
- The first request after a long idle period can be slow while the engine warms up; use a generous
request timeout (configurable) rather than a short default.
- Never retry 400, 401, 402, 403, 404 or 422.
BILLING
- Billed per character of text against the plan's monthly allowance.
- Repeating an identical request is served from cache and not billed again.
- Expose billingRemaining (from X-Billing-Remaining) so callers can warn before running out; an
exhausted allowance returns 402 when enforcement is on.
ACCEPTANCE CRITERIA
- synthesizeSpeech validates id, text length and parameter ranges locally, sends only documented
fields, and returns the audio bytes plus the parsed headers.
- A non-2xx response is never treated as audio; both the envelope and the flat billing body are
parsed into the typed error.
- 429/502 are retried with backoff; other 4xx are not.
- The key is never logged or sent from client-side code.
TEST PLAN
- Unit tests with mocked HTTP: 200 with audio/wav bytes and all four headers; 200 with only
Content-Type; 400 "Voice is not enrolled yet"; 402 quota_exhausted flat body; 404; 429 then 200;
502 then 200; 10,001-character text rejected locally; num_step 0 and 33 rejected locally.
- Smoke script:
- Get a ready voice id from FLAM_VOICE_ID, or by calling GET https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices (same
X-API-Key header; voices are in data.voices) and picking the first one whose status is "ready".
- Synthesize "Hello from Finesse." with output_format "wav", write speech.wav, and assert the
file starts with the ASCII bytes "RIFF" and Content-Type is audio/wav. Print the byte count,
X-Sample-Rate, X-Audio-URL and X-Billing-Remaining. Exit non-zero on error.
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.

