TypeScript SDK
Install @sandchest/sdk, transcribe a file in three lines, and use cancellation, idempotent retries and polling controls when you need them.
@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#
bun add @sandchest/sdknpm install @sandchest/sdkRequires Node 20 or newer, Bun, or any runtime with fetch, AbortSignal and
crypto.subtle.
The three-line version#
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#
new Sandchest({ apiKey, baseUrl, fetch });| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | — | Required. Sent as the Authorization header. A blank key throws immediately. |
baseUrl | string | https://stt-api.sandchest.com | Point this at your own deployment when self-hosting. A trailing slash is trimmed. |
fetch | function | globalThis.fetch | Swap in your own fetch for testing, tracing or proxying. |
client.transcripts#
transcribe(options, waitOptions?)#
Submit and wait. Equivalent to submit() followed by waitUntilReady().
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.
const { id } = await client.transcripts.submit({ audio: bytes });get(id, requestOptions?)#
Fetch the current state. Returns immediately; it does not wait.
const transcript = await client.transcripts.get(id);waitUntilReady(id, waitOptions?)#
Poll an existing transcript until it settles.
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.
const audioUrl = await client.files.upload(bytes);
const transcript = await client.transcripts.transcribe({ audio: audioUrl });TranscriptionOptions#
| Name | Type | Description |
|---|---|---|
audio | Blob | Uint8Array | ArrayBuffer | string | Required. Binary audio is uploaded for you; a string is used as audio_url directly. |
idempotencyKey | string | Sent as the Idempotency-Key header, and makes the upload idempotent too. |
language_code | string | Pin the language. See Languages. |
language_detection | boolean | Detect the language. Defaults to on when language_code is absent. |
language_detection_options | { expected_languages?, fallback_language? } | Narrow detection or name a fallback. |
speech_models | string[] | Accepted for compatibility and echoed back. |
punctuate | boolean | Punctuation and casing. Default true. |
format_text | boolean | Readable numbers, dates and currency. Default true. |
disfluencies | boolean | Keep "um" and "uh". Default false. |
custom_spelling | Array<{ from: string[] | string; to: string }> | null | Rewrite recognised words. |
multichannel | boolean | null | Transcribe each input channel separately. |
audio_start_from | number | null | Start offset in milliseconds. |
audio_end_at | number | null | End offset in milliseconds. |
WaitOptions#
| Name | Type | Default | Description |
|---|---|---|---|
pollingInterval | number | 75 | Milliseconds between polls. |
pollingTimeout | number | 1800000 | Give up after this many milliseconds — 30 minutes. |
signal | AbortSignal | — | Cancel 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.
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.
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.
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.status | Meaning |
|---|---|
| 400 | Bad request — an unknown option, an invalid value, or an unsupported feature |
| 401 | The API key is missing, malformed, revoked or expired |
| 402 | Out of credits |
| 409 | idempotencyKey conflict |
| 413 | The audio or the request body is too large |
| 429 | Rate limited |
| 408 | waitUntilReady hit pollingTimeout — "The transcription polling deadline expired." |
| 503 | A dependency is temporarily unavailable; safe to retry |
The full table, with what to do about each, is in Errors and limits.
Types#
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#
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);bun run verify.tscompleted 14 nvidia/parakeet-tdt-0.6b-v2Next#
Errors and limits — every status code, every limit, and what to do when you hit one.