Daily news brief voiceover: 1,400 characters a day for 30 days

A 1,400-character daily brief costs $0.0665 on Sume TTS, so 30 days is $1.995. Table for 1,000, 1,400 and 2,000 characters, plus a retry-safe daily job.

4 min readSume
All posts

A daily news brief of 1,400 characters costs $0.0665 to voice on Sume TTS, and a 30-day month of them costs $1.995. Sume bills TTS at $0.0475 per 1,000 characters, so the monthly bill is a straight multiplication of script length by days.

What a month of daily briefs costs

Each row is one TTS request per day at the listed length. The per-day price comes from the Sume price tool (TTS, customer USD), and the month column is that number times 30.

Sume TTS, one request per day, 30 days (read 2026-10-09)
Characters per briefPer day30 days
1,000$0.0475$1.425
1,400$0.0665$1.995
2,000$0.095$2.85

Submit one job per day with a date key

Use the date as the Idempotency-Key. If your scheduler fires twice, or your process dies after the POST, the second call with the same key and the same body returns the original job with idempotency_hit true instead of creating a second paid job. The fields below (transcript, avatar_handle, language) are the TTS 1.0 request shape from the OpenAPI document; the avatar must have a ready voice, which you can check on GET /v1/avatar-1.0/avatars.

Call speak() with a key like brief-2026-10-09, then audio_url() once the job is terminal. The result lists the audio file under result.artifacts.

import os, time, requests
API = "https://api.sume.com"
H = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}

def speak(text, key):
    body = {"transcript": text, "avatar_handle": "@narrator", "language": "en"}
    r = requests.post(f"{API}/v1/tts-1.0/generate", json=body,
                      headers={**H, "Idempotency-Key": key})
    r.raise_for_status()
    return r.json()["data"]["job"]["id"]

def audio_url(job_id):
    while True:
        s = requests.get(f"{API}/v1/jobs/{job_id}/status", headers=H).json()["data"]
        if s["terminal"]:
            break
        time.sleep(s["recommended_poll_interval_seconds"])
    if s["sume_status"] != "completed":
        raise RuntimeError(s["sume_status"])
    res = requests.get(f"{API}/v1/jobs/{job_id}/result", headers=H).json()["data"]["result"]
    return next(a["url"] for a in res["artifacts"] if a["type"] == "audio")

Limits and gotchas

One request takes at most 20,000 characters, so a 1,400-character brief is nowhere near the cap. Spaces and punctuation count toward usage, so count the final string you send, not the word count.

If you edit the script and resend it under the same date key, Sume answers 409 idempotency_conflict instead of re-voicing it. Use a new key such as brief-2026-10-09-v2 for a deliberate retake, and expect to pay for it as a new job.

The default output is mp3 at 44,100 Hz and 128 kbps. Set output_format only if a downstream tool needs wav or a different sample rate.

  • Exact price is the job's usage.billable_amount_usd on the submit response.
  • A 30-second sync wait is available with mode sync, but poll status_url if the job is not done by then.

Sources

Related posts

More in Use cases

All Use cases posts

Written by Sume