TTS voice list returns 503: provider_unavailable vs not_configured
GET /v1/tts-router/voices can return 503 two ways on Sume. provider_unavailable is a retry; provider_not_configured is not. Cache, timeout and a retry loop.

If GET /v1/tts-router/voices answers 503, read the error code before you retry. provider_unavailable means the voice list could not be read from the provider right now, so wait a few seconds and ask again. provider_not_configured means that voice family is not set up in the environment you are calling, and retrying will not change it.
The route takes one required query parameter, family, which is sonic (for the sonic-* models) or eleven (for the eleven-* models). Each model in GET /v1/tts-router/models names its list in voice_family, so read that field instead of guessing.
What do the two 503 errors mean?
The two failures come from different places in the route, and they carry different bodies.
Treat the status code alone as ambiguous. Both errors are 503, so a generic gateway rule such as retry every 5xx will hammer a configuration problem that no amount of waiting fixes. Branch on code.
| Code | Meaning | Retry? | Extra detail |
|---|---|---|---|
| provider_unavailable | The provider list call failed, timed out, or returned something that was not JSON | Yes, after a short wait | family in the error details |
| provider_not_configured | The voice provider has no key in this environment | No | A missing list naming the component |
How long is a voice list cached?
A successful read is cached for ten minutes per family, so repeated calls inside that window return the same list without another provider call. A failed read is not cached: the next request tries the provider again. That is why a plain retry is safe and why one bad moment does not stick for ten minutes.
The provider call itself has an 8-second timeout. A slow provider therefore turns into provider_unavailable rather than a hung request. Your own HTTP client timeout should sit above 8 seconds, or you will give up before the API does.
Which list did I get back?
A 200 response carries source. For family=sonic it is live, and only public voices are listed; your own cloned voices stay in the Voices library under voi_* ids and work with sonic-* models. For family=eleven it is live when an ElevenLabs account is connected to the environment, and premade otherwise. A premade list is a fixed set of 21 stock voices, not the whole ElevenLabs library, so do not treat it as an error when a voice you expect is missing.
A retry loop that stops on the right error
This sketch retries only the transient case, with a growing wait, and fails fast on anything else. It reads the code from the error body, so it never retries a configuration problem by accident.
import os
import time
import requests
URL = "https://api.sume.com/v1/tts-router/voices"
HEADERS = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}
def list_voices(family: str, tries: int = 4) -> dict:
for attempt in range(tries):
r = requests.get(URL, headers=HEADERS,
params={"family": family}, timeout=15)
if r.status_code == 200:
return r.json()["data"]
body = r.json() if r.content else {}
code = str(body.get("error", {}).get("code", ""))
if r.status_code == 503 and code == "provider_unavailable":
time.sleep(2 * (attempt + 1))
continue
r.raise_for_status()
raise RuntimeError(f"{family} voices still unavailable")
if __name__ == "__main__":
result = list_voices("sonic")
print(result["source"], len(result["voices"]))
Does a failed list block generation?
No. The list is a lookup, not a step in a job. If you already hold a voice id, POST /v1/tts-router/generate does not need the list call first, so a cached id in your own config keeps working while the list is down.
The same goes for the models catalog: GET /v1/tts-router/models is a separate call, and its entries tell you each model's character limit, languages and output formats. Cache both lookups at startup and refresh them on a timer, with the list refresh no more often than the ten-minute cache would give you anything new.
Sources
Related posts
More in Developers
- Usage hold_counts: open, browser_session, billing_pending
A scoped Sume usage summary can show final false with money held. hold_counts says whether a turn runs, a Browser session is open, or work is being priced.
- Vapor 4: collect the body, verify a Sume webhook with swift-crypto
Use body: .collect(maxSize:) in Vapor 4, read req.body.data, and verify the Sume HMAC SHA-256 signature with swift-crypto before returning .noContent.
- Vert.x Web: verify a Sume webhook after BodyHandler reads the body
Install BodyHandler before your Vert.x route, read the body as bytes, recompute HMAC SHA-256 over timestamp.body, and return 204 to a Sume delivery.
- What one script_run call cost: the usage script_runs array
Sume's usage summary lists each script_run call with its rows, debited, held and refunded micros, so a fan-out of TTS or image jobs has one price.
Written by Sume