Skip to content

TypeScript SDK

Install @sandchest/sdk, transcribe a file in three lines, and use cancellation, idempotent retries and polling controls when you need them.

Open .md

@sandchest/sdk is the native client: fully typed, no runtime dependencies, and it propagates cancellation through uploads, requests and polling. It talks to the native endpoints (/api/v1/transcripts). This page is its complete API.

Install#

shell
bun add @sandchest/sdk
shell
npm install @sandchest/sdk

Requires Node 20 or newer, Bun, or any runtime with fetch, AbortSignal and crypto.subtle.

The three-line version#

transcribe.ts
import { readFile } from "node:fs/promises";
import { Sandchest } from "@sandchest/sdk";

const client = new Sandchest({ apiKey: process.env.SANDCHEST_API_KEY as string });

const transcript = await client.transcripts.transcribe({
  audio: await readFile("meeting.mp3"),
});

for (const word of transcript.words ?? []) {
  console.log(word.start, word.end, word.text);
}

transcribe() uploads the bytes, creates the transcript, and polls until it is completed or error. It resolves with the transcript in either state — check status before trusting words.

The client#

TypeScript
new Sandchest({ apiKey, baseUrl, fetch });
OptionTypeDefaultDescription
apiKeystringRequired. Sent as the Authorization header. A blank key throws immediately.
baseUrlstringhttps://stt-api.sandchest.comPoint this at your own deployment when self-hosting. A trailing slash is trimmed.
fetchfunctionglobalThis.fetchSwap in your own fetch for testing, tracing or proxying.

client.transcripts#

transcribe(options, waitOptions?)#

Submit and wait. Equivalent to submit() followed by waitUntilReady().

TypeScript
const transcript = await client.transcripts.transcribe(
  { audio: bytes, disfluencies: true },
  { pollingInterval: 500, signal: controller.signal },
);

submit(options, requestOptions?)#

Upload if needed and create the transcript. Returns as soon as the job is queued.

TypeScript
const { id } = await client.transcripts.submit({ audio: bytes });

get(id, requestOptions?)#

Fetch the current state. Returns immediately; it does not wait.

TypeScript
const transcript = await client.transcripts.get(id);

waitUntilReady(id, waitOptions?)#

Poll an existing transcript until it settles.

TypeScript
const transcript = await client.transcripts.waitUntilReady(id, { pollingTimeout: 120_000 });

client.files#

upload(audio, requestOptions?)#

Store bytes and get back a URL string, when you want to manage the two steps yourself.

TypeScript
const audioUrl = await client.files.upload(bytes);
const transcript = await client.transcripts.transcribe({ audio: audioUrl });

TranscriptionOptions#

NameTypeDescription
audioBlob | Uint8Array | ArrayBuffer | stringRequired. Binary audio is uploaded for you; a string is used as audio_url directly.
idempotencyKeystringSent as the Idempotency-Key header, and makes the upload idempotent too.
language_codestringPin the language. See Languages.
language_detectionbooleanDetect the language. Defaults to on when language_code is absent.
language_detection_options{ expected_languages?, fallback_language? }Narrow detection or name a fallback.
speech_modelsstring[]Accepted for compatibility and echoed back.
punctuatebooleanPunctuation and casing. Default true.
format_textbooleanReadable numbers, dates and currency. Default true.
disfluenciesbooleanKeep "um" and "uh". Default false.
custom_spellingArray<{ from: string[] | string; to: string }> | nullRewrite recognised words.
multichannelboolean | nullTranscribe each input channel separately.
audio_start_fromnumber | nullStart offset in milliseconds.
audio_end_atnumber | nullEnd offset in milliseconds.

WaitOptions#

NameTypeDefaultDescription
pollingIntervalnumber75Milliseconds between polls.
pollingTimeoutnumber1800000Give up after this many milliseconds — 30 minutes.
signalAbortSignalCancel the wait, and the in-flight request with it.

Cancellation#

Every method takes an AbortSignal, and it reaches all the way down: the upload, the create request, the poll loop and the request in flight when you abort.

TypeScript
const controller = new AbortController();
setTimeout(() => controller.abort(), 30_000);

try {
  const transcript = await client.transcripts.transcribe(
    { audio: bytes },
    { signal: controller.signal },
  );
} catch (error) {
  if (error instanceof DOMException && error.name === "AbortError") {
    console.log("Gave up waiting.");
  }
}

Aborting stops your client from waiting. It does not cancel the transcript — the job keeps running and is still billed on completion. Keep the id if you may want the result later.

Idempotent retries#

Pass idempotencyKey and a retry is free of duplicates. Sandchest returns the original transcript instead of creating a second one, and the SDK caches the upload so the same bytes are not sent twice.

retry.ts
import { SandchestError } from "@sandchest/sdk";

async function transcribeWithRetry(bytes: Uint8Array, key: string) {
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      return await client.transcripts.transcribe({ audio: bytes, idempotencyKey: key });
    } catch (error) {
      if (error instanceof SandchestError && error.status >= 500 && attempt < 2) continue;
      throw error;
    }
  }
  throw new Error("unreachable");
}

Use a key derived from your domain — a job id, a recording id, a content hash — not a random value, or a retry after a crash will not match. Keys are at most 200 characters, scoped to your workspace, and never expire.

The upload cache holds at most 128 entries for five minutes each. After it expires, re-uploading the identical bytes still matches: Sandchest compares a persistent SHA-256 fingerprint of the audio, not the URL. Genuinely different bytes under the same key are a 409.

Errors#

Every non-2xx response becomes a SandchestError carrying the server's message and status code.

TypeScript
import { Sandchest, SandchestError } from "@sandchest/sdk";

try {
  await client.transcripts.transcribe({ audio: bytes });
} catch (error) {
  if (error instanceof SandchestError) {
    console.error(error.status, error.message);
    if (error.status === 402) await topUpCredits();
    if (error.status === 429) await backOff();
  } else {
    throw error;
  }
}
error.statusMeaning
400Bad request — an unknown option, an invalid value, or an unsupported feature
401The API key is missing, malformed, revoked or expired
402Out of credits
409idempotencyKey conflict
413The audio or the request body is too large
429Rate limited
408waitUntilReady hit pollingTimeout"The transcription polling deadline expired."
503A dependency is temporarily unavailable; safe to retry

The full table, with what to do about each, is in Errors and limits.

Types#

TypeScript
import type {
  Transcript,
  TranscriptWord,
  TranscriptUtterance,
  TranscriptStatus,
  TranscriptionOptions,
  WaitOptions,
  AudioInput,
} from "@sandchest/sdk";

TranscriptWord is { text, start, end, confidence, speaker, channel } with start and end in milliseconds. The full transcript shape is documented in Transcript options.

Verify#

verify.ts
import { Sandchest } from "@sandchest/sdk";

const client = new Sandchest({ apiKey: process.env.SANDCHEST_API_KEY as string });
const transcript = await client.transcripts.transcribe({
  audio: await Bun.file("meeting.mp3").bytes(),
});

console.log(transcript.status, transcript.words?.length, transcript.speech_model_used);
shell
bun run verify.ts
expected output
completed 14 nvidia/parakeet-tdt-0.6b-v2

Next#

Errors and limits — every status code, every limit, and what to do when you hit one.