Idempotency keys for batch TTS: derive them from what you render
A retry loop that mints random keys pays twice. Hash the sentence, voice, model and settings into the key, and re-running a script returns the first job.

How do you avoid paying twice when a batch TTS script crashes halfway? Derive each Idempotency-Key from what the request renders, not from a counter or a random id. Re-running the script then sends the same keys, and the API returns the existing jobs.
Cheap generation makes big batches normal. Mistral's Voxtral TTS (read 2026-10-04) is priced at $0.016 per 1,000 characters, and a book-length script is hundreds of requests. A crash at request 212 should cost nothing on restart.
What the key must include
If two requests would produce different audio, their keys must differ. If they would produce the same audio, their keys should match.
- The exact transcript text for the sentence.
- The voice id or avatar handle, and the language.
- The model id when you use the router.
- Generation settings: speed, volume and emotion.
- A version string you bump deliberately when you want a fresh take.
A key function
Hash a canonical form of those inputs. Sorted keys keep the hash stable across runs. Every Sume submit also reports idempotency_hit, so you can log which sentences were already rendered.
import hashlib, json
def tts_key(text, voice, model, config, version="v1"):
blob = json.dumps({"t": text, "v": voice, "m": model,
"c": config, "ver": version},
sort_keys=True, ensure_ascii=False)
return "tts-" + hashlib.sha256(blob.encode("utf-8")).hexdigest()[:40]
a = tts_key("Hello there.", "voi_demo", "sonic-3.6", {"speed": 1.0})
b = tts_key("Hello there.", "voi_demo", "sonic-3.6", {"speed": 1.0})
c = tts_key("Hello there.", "voi_demo", "sonic-3.6", {"speed": 1.1})
print(a == b, a == c)
Edge cases
Reusing a key with a different body is a conflict, not a new job, which is what you want: a changed sentence must get a changed key. Do not put timestamps or run numbers in the key, since those change on every restart and defeat the point.
A mode of sync or subscribe waits at most 30 seconds, so a slow sentence returns before it is done. Poll the status_url with the same job instead of resubmitting, and the key stays valid for the whole batch.
Sources
Related posts
More in Developers
- Ideogram 4.5 edits keep the source shape; GPT Image 2.5 needs auto
On Sume an Ideogram 4.5 edit without aspect_ratio keeps the source image's shape, while other edit calls should send aspect_ratio auto. What to send for each.
- Image brief template: consistent Sume prompts with string.Template
Stop rewriting prompts by hand. Keep subject, style, lighting and constraints as fields, fill them from data and send the same shape to Sume's image endpoint.
- Image edit reference URL rejected? Public HTTPS checklist for Sume
Sume rejects localhost, private-network and non-HTTPS reference URLs, and a URL it cannot download fails as input_media_unreachable. Check yours in 12 lines.
- Image job progress bar: Sume has no percentage, so show job stages
ChatGPT Images 2.5 shows a progress percentage. Sume image jobs report queued, processing and completed plus events, and stream:true returns 400.
Written by Sume