JavaScript / TypeScript SDK

Official JS/TS client for the Speech Revolutions STT API. Async-first (like the Deepgram / ElevenLabs JS clients) and runs on Node 18+ using native fetch.

Source on GitHub · report an issue · npm

Install

bash
npm install speechrevolutions

Quickstart

transcribe() accepts a local path, a URL, raw bytes, or a Blob. With the default outputType: "json" the SDK parses the response into a transcript-first result object.

transcribe.ts
import { SpeechRevolutions } from "speechrevolutions";

const client = new SpeechRevolutions(); // SPEECHREVOLUTIONS_API_KEY
const result = await client.transcribe("meeting.mp3", { speakerLabels: true });

console.log(result.text);
for (const u of result.utterances) {
  console.log(`Speaker ${u.speaker}: ${u.text}`);
}

From a URL or file

transcribe() auto-detects http(s) URLs; the transcribeUrl() alias makes intent explicit.

ts
// auto-detected
const result = await client.transcribe("https://example.com/audio.mp3");

// explicit alias
const result2 = await client.transcribeUrl("https://example.com/audio.mp3");

Options

Pass options as the second argument. diarize is a Deepgram-compatible alias for speakerLabels.

ts
await client.transcribe("a.mp3", {
  diarize: true,        // alias for speakerLabels
  outputType: "srt",
  wordTimestamps: true,
  customVocabulary: ["AcmeCorp"],
});
OptionTypeDefault
outputType"txt" | "json" | "srt" | "vtt" | "docx" | "pdf""json"
wordTimestampsbooleantrue
speakerLabelsbooleantrue
diarizeboolean (alias for speakerLabels)
nltkboolean (punctuation & capitalization)true
tier"standard""standard"
customVocabularystring[]undefined
onProgress(event: ProgressEvent) => voidundefined
onUploadProgress(event: ProgressEvent) => voidundefined
progressboolean (render console bars)false

tier: standard is the only tier currently available.

Live progress

Unlike AssemblyAI and Deepgram — which expose no percentage for pre-recorded audio — you get real-time progress for both the file upload and the transcription, as a console bar, a callback, or both. They compose: the bars render and your callbacks still fire for every event.

ts
// 1. Console bars — an "Uploading" byte bar, then a "Transcribing" bar,
//    rendered to stderr on a single carriage-return-updated line.
await client.transcribe("meeting.mp3", { progress: true });

// 2. Programmatic — read event.percent (0-100, undefined until total is known)
await client.transcribe("meeting.mp3", {
  onProgress(event) {
    // transcription: event.step is e.g. "transcribe"
    console.log(event.percent, event.step);
  },
  onUploadProgress(event) {
    // upload: event.step === "upload"; completed / total are bytes
    console.log("upload", event.percent);
  },
});

// onProgress may also be passed as a 3rd positional argument.

Each ProgressEvent carries completed, total, step, and a computed percent (0–100, undefined when the total is not yet known).

Building a UI? Live progress for web apps shows how to fold both callbacks into a single 0–100 bar you can serve to your frontend.

Result shape

With outputType: "json" the SDK returns a transcript-first object:

MemberDescription
result.textFull transcript (AssemblyAI / ElevenLabs style)
result.transcriptDeepgram-style alias
result.wordsWord + start / end / speaker
result.utterancesAssemblyAI-style speaker turns
result.toDeepgram()Deepgram-shaped object
result.toDict()Normalized JSON
result.content / result.save(path)Raw bytes / write to disk
ts
const dg = result.toDeepgram();
console.log(dg.results.channels[0].alternatives[0].transcript);

// save() appends the output type when the path has no extension.
// Returns the written path.
const path = await result.save("output"); // -> "output.json"

Webhooks

Pass callbackUrl to be notified when a job finishes instead of holding the call open — the right pattern for server and background workloads. On completion or permanent failure the platform POSTs a signed JSON body to your URL:

ts
await client.transcribe("meeting.mp3", {
  callbackUrl: "https://you.example.com/hook",
});

The POST body is { job_id, status: "completed"|"failed", download_url?, step?, reason? }, signed with HMAC-SHA256 over the raw body in the X-SR-Signature: sha256=<hex> header (plus X-SR-Event with the status and a unique X-SR-Delivery id). Verify against the raw body bytes with a constant-time comparison.

webhooks.ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifySignature(rawBody, signatureHeader, signingSecret) {
  const expected =
    "sha256=" + createHmac("sha256", signingSecret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}

// Express handler — capture the RAW body for verification.
// app.post(
//   "/webhooks/speechrevolutions",
//   express.raw({ type: "application/json" }),
//   (req, res) => {
//     if (!verifySignature(req.body, req.get("X-SR-Signature"), SECRET))
//       return res.status(401).send("bad signature");
//     const event = JSON.parse(req.body.toString());
//     if (event.status === "completed") {
//       // mark done; fetch event.download_url
//     } else {
//       // event.step, event.reason
//     }
//     res.json({ ok: true }); // a 2xx acks delivery (we retry on 5xx)
//   },
// );

Retrieve results later

Fetch a job by id anytime — useful after a webhook, or when rebuilding state after a restart. The download URL is regenerated on demand, so results are fetchable long after the original upload.

retrieve.ts
// Get a job's current status ("processing" | "completed" | "failed")
const status = await client.getJobStatus(jobId);
console.log(status.status);

if (status.status === "completed") {
  const result = await client.getTranscript(jobId); // downloads + parses
  console.log(result.text);
} else if (status.status === "failed") {
  console.log(status.failedStage, status.reason);
}

// List most-recent jobs (newest first), cursor-paginated
const page = await client.listJobs({ limit: 50 });
console.log(page.jobs, page.nextBefore);
for (const job of page.jobs) console.log(job.jobId, job.createdAt);

Robustness

Configure retries, backoff, and low-level request options on the client. Transient 429/5xx/network errors are retried automatically (honoring the Retry-After header).

ts
import { ProxyAgent } from "undici";

// Any fetch option can be passed through; a dispatcher is how undici proxies.
const dispatcher = new ProxyAgent("http://proxy.internal:8080");

const client = new SpeechRevolutions({
  maxRetries: 3,
  retryBackoffMs: 500,  // exponential
  requestInit: { dispatcher },
});

Errors are typed and carry a .statusCode and the server .requestId for correlating with support.

ts
import { RateLimitError, AuthenticationError } from "speechrevolutions";

try {
  const result = await client.transcribe("meeting.mp3");
} catch (err) {
  if (err instanceof RateLimitError) {
    console.log(err.statusCode, err.requestId); // e.g. 429 "req_..."
  }
}

Auth

The SDK reads the key from either environment variable:

bash
export SPEECHREVOLUTIONS_API_KEY=stt_...

Or pass it explicitly:

ts
const client = new SpeechRevolutions({ apiKey: "stt_..." });
// or the shorthand
const client2 = new SpeechRevolutions("stt_...");

The upload is streamed in chunks with an explicit Content-Length (so presigned S3 PUTs never see Transfer-Encoding: chunked), and the SDK converts the server's SSE completed/total counts into percent for you.