# 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

```bash
bun add @sandchest/sdk
```

```bash
npm install @sandchest/sdk
```

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

## The three-line version

```ts title="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

```ts
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()`.

```ts
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.

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

### get(id, requestOptions?)

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

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

### waitUntilReady(id, waitOptions?)

Poll an existing transcript until it settles.

```ts
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.

```ts
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](/docs/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.

```ts
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.

```ts title="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.

```ts
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](/docs/errors).

> [!IMPORTANT]
> A transcript that fails during inference is **not** an exception. It resolves with
> `status: "error"` and a populated `error` field. Always check `status` before
> reading `words`.

## Types

```ts
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](/docs/transcripts#the-transcript-object).

## Verify

```ts title="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);
```

```bash
bun run verify.ts
```

```text title="expected output"
completed 14 nvidia/parakeet-tdt-0.6b-v2
```

## Next

[Errors and limits](/docs/errors) — every status code, every limit, and what to do
when you hit one.
