Text to speech API in JavaScript: save an MP3 from Node.js
Call a text to speech API from JavaScript on your server: submit the text with the Sume SDK in Node.js, wait for the job, then save the MP3.

For text to speech in JavaScript, pick between two routes. In the browser, the Web Speech API's speechSynthesis reads text aloud, normally through the device's default speech synthesizer. When you need an audio file to store or serve, call a hosted text to speech API from Node.js on your server: send the text and a voice, wait for the job, and download the MP3. With Sume's TypeScript SDK, that is generateTtsV1 (POST /v1/tts-1.0/generate), then waitForJob, then a fetch of the audio file.
The Sume steps come from the TypeScript SDK and Waiting for runs and jobs docs and the TTS 1.0 schema in the Sume API reference, read on 2026-09-27. The browser API is described from MDN's Web Speech API page. Every TTS request field, in curl, is in Text to speech API.
Why call text to speech from Node.js instead of the browser?
Because the API key has to stay on your server. The SDK docs say a Sume key spends your credits and has no browser-safe variant: never ship one to client JavaScript, a mobile bundle, or a NEXT_PUBLIC_* variable. Put your own endpoint in front and make the Sume call there. The CORS guide covers what happens when a web page calls the API directly.
Install the client with npm install @sume-com/sdk. It needs fetch and WebCrypto, and runs on Node 18+, Bun, Deno, or Cloudflare Workers.
How do I convert text to an MP3 in Node.js?
Create one client, submit the text with a voice, wait for the job, and download the file. With no output_format, TTS 1.0 returns an MP3 at 44,100 Hz and 128 kbps. With no mode, the submit is async, which is how waitForJob expects jobs to be submitted.
transcripttakes 1–20,000 characters. The voice is theavatar_idoravatar_handleof an avatar whose voice is ready, or avoice.idyou already hold; Text to speech API covers finding one. Setlanguagefor any non-English text.- The
idempotency-keyheader makes the submit safe to retry. The client retries aPOSTonly when it carries one, and a retry with the same key and body returns the original job instead of billing a second one. - Generated operations don't throw on an API error: they resolve with
{ data, error, response }, so checkerrorfirst.waitForJobalso returns failed and canceled jobs, so checkjob.statusbefore reading the result. - The audio is the entry with
type: "audio"inresult.artifacts[]. Itsurlis a public Sume CDN URL, so a plainfetchdownloads it.
import { writeFile } from "node:fs/promises";
import { createSumeClient, generateTtsV1, waitForJob } from "@sume-com/sdk";
const client = createSumeClient({ apiKey: process.env.SUME_API_KEY! });
const { data, error } = await generateTtsV1({
client,
headers: { "idempotency-key": "shipping-notice-001" },
body: {
transcript: "Your order has shipped. It should arrive on Thursday.",
avatar_handle: "narrator",
language: "en",
},
});
if (error) throw new Error(JSON.stringify(error));
const job = await waitForJob(data!.data.request_id, { client });
if (job.status !== "completed") throw new Error(`TTS job ${job.status}`);
const audio = job.result?.artifacts?.find((a) => a.type === "audio");
const file = await fetch(audio!.url);
await writeFile("shipping-notice.mp3", Buffer.from(await file.arrayBuffer()));Which SDK calls does a TTS job use?
Each step maps to one SDK call and one HTTP route. Generated functions are named after the OpenAPI operation ids, and the docs point to your editor's autocomplete for the full list. Wait for a Sume job or run to finish covers the wait helper's options and errors.
| Step | SDK call | HTTP route | What to know |
|---|---|---|---|
| Submit | generateTtsV1 | POST /v1/tts-1.0/generate | The response carries the job id as request_id. |
| Wait | waitForJob | GET /v1/jobs/:id/status | Waits at least 2 seconds between polls, longer when next_poll_after_seconds asks, and times out after 20 minutes by default. |
| Read later | getApiJob | GET /v1/jobs/:id | A wait that times out doesn't cancel the job. It keeps running and still bills. |
| Cancel | cancelApiJob | POST /v1/jobs/:id/cancel | Succeeds only before generation starts. After that the job runs to completion. |
Can I stream the speech instead of waiting for a file?
Not with TTS 1.0. The API reference describes it as an async job with polling or a webhook, non-streaming. mode: "sync" holds the HTTP request for at most 30 seconds, and /result answers 409 job_not_completed until result_ready is true, so an async submit plus waitForJob is the simple path.
To skip polling, send a public HTTPS webhook_url with the submit. Delivery is terminal-only (job.completed, job.failed, or job.canceled), the SDK's verifyWebhook checks the signature (Verifying webhooks), and the API reference says to keep polling available as a backup.
How long can the text be, and what does it cost?
One request takes up to 20,000 characters, and synthesized audio longer than 1,200 seconds fails with tts_duration_exceeded, with no credit capture. Split a longer script into several jobs.
TTS 1.0 costs $0.0475 per 1,000 characters, plus a 5.5% agent fee by default, and spaces and punctuation count toward usage. How Sume pricing works explains the wallet.
Sources
Related posts
More in Developers
- Text to speech with highlighted words: sync word timings
Highlight words as text to speech plays: get word timestamps with the audio, then mark the word whose time range holds the player's current time.
- Webhook URL rejected as invalid? Sume's webhook URL rules
Sume answers 400 invalid_request when a webhook URL is not public HTTPS. The rules for scheme, host, port, and credentials, and the check at delivery.
- Connect Claude Code, Cursor, or Codex to Sume with hosted MCP
Sume's hosted MCP server at mcp.sume.com/mcp lets coding agents generate images, video, audio, and avatars. Setup, OAuth scopes, and spend gates.
- Idempotency keys for AI video APIs: retry without paying twice
An idempotency key makes a retried create return the original run or job instead of a second paid one. How Sume's Idempotency-Key works on each API.
Written by Sume