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.

5 min readSume
All posts

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.

  • transcript takes 1–20,000 characters. The voice is the avatar_id or avatar_handle of an avatar whose voice is ready, or a voice.id you already hold; Text to speech API covers finding one. Set language for any non-English text.
  • The idempotency-key header makes the submit safe to retry. The client retries a POST only 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 check error first. waitForJob also returns failed and canceled jobs, so check job.status before reading the result.
  • The audio is the entry with type: "audio" in result.artifacts[]. Its url is a public Sume CDN URL, so a plain fetch downloads 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.

From TypeScript SDK, Waiting for runs and jobs, Jobs and results, and the Sume API reference, read 2026-09-27.
StepSDK callHTTP routeWhat to know
SubmitgenerateTtsV1POST /v1/tts-1.0/generateThe response carries the job id as request_id.
WaitwaitForJobGET /v1/jobs/:id/statusWaits at least 2 seconds between polls, longer when next_poll_after_seconds asks, and times out after 20 minutes by default.
Read latergetApiJobGET /v1/jobs/:idA wait that times out doesn't cancel the job. It keeps running and still bills.
CancelcancelApiJobPOST /v1/jobs/:id/cancelSucceeds 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

All Developers posts

Written by Sume