Get a job
curl --request GET \
--url https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{id} \
--header 'X-API-Key: <api-key>'import requests
url = "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{id}"
headers = {"X-API-Key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-Key': '<api-key>'}};
fetch('https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{id}', 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/jobs/{id}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"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"
"net/http"
"io"
)
func main() {
url := "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{id}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{id}")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-Key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"data.id": "<string>",
"data.voice_id": "<string>",
"data.job_type": "<string>",
"data.status": "<string>",
"data.progress": 123,
"data.error": {}
}Endpoints
Get a job
Poll the status of an async clone/enroll job.
GET
/
flam-voice-svc
/
api
/
v1
/
public
/
jobs
/
{id}
Get a job
curl --request GET \
--url https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{id} \
--header 'X-API-Key: <api-key>'import requests
url = "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{id}"
headers = {"X-API-Key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-Key': '<api-key>'}};
fetch('https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{id}', 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/jobs/{id}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"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"
"net/http"
"io"
)
func main() {
url := "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{id}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{id}")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-Key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"data.id": "<string>",
"data.voice_id": "<string>",
"data.job_type": "<string>",
"data.status": "<string>",
"data.progress": 123,
"data.error": {}
}Returns the status of an asynchronous clone job started by
/clone or
/enroll. Poll this endpoint until status reaches a terminal state. The
job must belong to the API key’s workspace, otherwise the request returns 404.
Authorization
string
required
Your workspace API key (
flv_…).Path parameters
string
required
The
job_id returned by /clone or /enroll.Response
string
The job id.
string
The voice this job is enrolling.
string
The kind of job, e.g.
instant_clone, pro_train, verify, localize, design_preview,
reembed.string
Job state.
pending or running while in progress; terminal states are succeeded, failed,
cancelled, and expired. When succeeded, the voice’s status is ready.integer
Completion percentage,
0–100.object
Present when
status is failed - carries the provider error detail. A reference clip the cloner
can’t use (it needs at least ~6 seconds of continuous speech) fails the job this way.curl "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/665f1a2b3c4d5e6f7a8b9aa01" \
-H "X-API-Key: $FLAM_API_KEY"
const jobId = "665f1a2b3c4d5e6f7a8b9aa01";
const res = await fetch(
`https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/${jobId}`,
{ headers: { "X-API-Key": process.env.FLAM_API_KEY } },
);
const { data } = await res.json();
console.log(data.status, data.progress);
import os, requests
job_id = "665f1a2b3c4d5e6f7a8b9aa01"
res = requests.get(
f"https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{job_id}",
headers={"X-API-Key": os.environ["FLAM_API_KEY"]},
)
print(res.json()["data"]["status"])
Response
{
"status": 200,
"data": {
"id": "665f1a2b3c4d5e6f7a8b9aa01",
"voice_id": "665f1a2b3c4d5e6f7a8b9c0d",
"org_id": "664a0b1c2d3e4f5a6b7c8d9e",
"job_type": "instant_clone",
"status": "succeeded",
"progress": 100
},
"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: "Get a job".
Everything you need is in this prompt. Do not invent anything that is not specified here.
WHAT TO BUILD
- A function getJob(jobId) (name it per the project's conventions) that returns the status of an
asynchronous clone job started by POST /flam-voice-svc/api/v1/public/voices/clone or POST /flam-voice-svc/api/v1/public/voices/{id}/enroll.
- A function waitForJob(jobId, options) that polls getJob until the job is terminal.
- 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: GET
- URL: https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{id}
PARAMETERS
- id (path, string, required): the job_id returned by /clone or /enroll (a 24-character hex id).
URL-encode it and reject an empty id locally.
- No query or body parameters.
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"
}
SUCCESS RESPONSE (200, application/json)
JOB OBJECT (returned by GET https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{id} in "data")
- id: string. The job id.
- voice_id: string. The voice this job is enrolling.
- org_id: string. The owning workspace.
- job_type: string, e.g. "instant_clone", "pro_train", "verify", "localize", "design_preview",
"reembed".
- status: "pending" | "running" while in progress; terminal states are "succeeded", "failed",
"cancelled" and "expired". When "succeeded", the voice's status is "ready".
- progress: integer 0-100.
- error: object, present when status is "failed"; carries the provider error detail.
{
"status": 200,
"data": {
"id": "665f1a2b3c4d5e6f7a8b9aa0",
"voice_id": "665f1a2b3c4d5e6f7a8b9c0d",
"org_id": "664a0b1c2d3e4f5a6b7c8d9e",
"job_type": "instant_clone",
"status": "succeeded",
"progress": 100
},
"error": false
}
Type status as a union of the six documented values but tolerate unknown strings (keep polling
until timeout rather than crashing).
POLLING THE JOB
- Poll GET https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{job_id} (same X-API-Key header) every ~2 seconds until
data.status is terminal: "succeeded", "failed", "cancelled" or "expired". "pending" and "running"
mean keep polling.
- On "succeeded" the voice is "ready" and can synthesize. On "failed" surface data.error. On
"cancelled" or "expired" surface a clear "clone did not complete" error.
- Make the interval and an overall timeout configurable, accept an abort/cancel signal, and retry
individual poll requests on 429, 502 and network errors with backoff (a poll is a safe GET).
- A 404 while polling means the job does not exist in this workspace: stop and surface it.
ERRORS
- 401: X-API-Key missing, malformed, invalid or revoked. Do not retry.
- 404: the job does not exist or belongs to another workspace. Do not retry.
- 429: rate limited. Retry with backoff.
- 502: upstream 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
- Retry only 429 (rate limited), 502 (provider unavailable or warming up) and network errors or
timeouts, with exponential backoff plus jitter (for example about 1s, 2s, 4s; at most 3 retries),
then surface the error.
- Never retry 400, 401, 402, 403, 404 or 422: they fail the same way again.
ACCEPTANCE CRITERIA
- getJob returns the typed job from "data".
- waitForJob resolves with the job on "succeeded"; rejects with a descriptive error on "failed"
(including data.error), "cancelled", "expired", or when its configurable timeout elapses; stops
promptly when aborted; and reports progress (0-100) to an optional callback.
- 404 and 401 are not retried; transient poll failures are.
TEST PLAN
- Unit tests with mocked HTTP and fake timers: pending -> running(40) -> succeeded(100); running
-> failed with an error object; expired; timeout; abort; 404; 502 then 200.
- Smoke script: given FLAM_JOB_ID, print status and progress once, or poll with --wait until
terminal. 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.

