# Quickstart

Get an API key, set one environment variable, and turn a local audio file into word-level timestamps — with the AssemblyAI SDK, the native SDK, or curl.

By the end of this page you will have a Sandchest API key in your environment and a
completed transcript printed as words with millisecond timings. Pick one of the three
paths in step 3; they all produce the same transcript.

## 1. Get an API key

1. Open [sandchest.com/dashboard](https://sandchest.com/dashboard) and sign in. Sign-in
   is a six-digit code sent to your email — there is no password.
2. Go to **API keys** and create a key. Give it a name you will recognise later
   (`local dev`, `staging worker`).
3. Copy it now. Keys start with `sc_live_` and are shown exactly once; Sandchest stores
   only a hash, so a lost key must be replaced rather than recovered.

> [!WARNING]
> An API key grants full access to your workspace's audio, transcripts and credits.
> Keep it server-side. Never ship it to a browser, a mobile app or a public repository.

## 2. Put the key in your environment

```bash
export SANDCHEST_API_KEY="sc_live_..."
```

For a project, put it in `.env.local` (or `.env`) and make sure that file is
git-ignored:

```bash title=".env.local"
SANDCHEST_API_KEY=sc_live_...
```

Check it is set:

```bash
echo "${SANDCHEST_API_KEY:0:8}"
```

```text title="expected output"
sc_live_
```

## 3. Transcribe a file

You need a local audio or video file. Anything FFmpeg can decode works —
`.mp3`, `.m4a`, `.wav`, `.mp4`, `.webm`. The examples below use `meeting.mp3`.

### With the AssemblyAI SDK

If your project already uses `assemblyai`, keep it. Change the key and the base URL.

```bash
npm install assemblyai   # or: bun add assemblyai
```

```ts title="transcribe.ts"
import { readFile } from "node:fs/promises";
import { AssemblyAI } from "assemblyai";

const client = new AssemblyAI({
  apiKey: process.env.SANDCHEST_API_KEY as string,
  baseUrl: "https://stt-api.sandchest.com",
});

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

if (transcript.status === "error") {
  throw new Error(transcript.error ?? "Transcription failed.");
}

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

`transcribe()` uploads the bytes, creates the transcript, and polls until it is
finished. The AssemblyAI SDK owns the polling loop — Sandchest's `GET` returns the
current state immediately rather than holding the connection open.

### With `@sandchest/sdk`

```bash
bun add @sandchest/sdk   # or: npm install @sandchest/sdk
```

```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"),
});

if (transcript.status === "error") {
  throw new Error(transcript.error ?? "Transcription failed.");
}

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

The base URL defaults to `https://stt-api.sandchest.com`; pass `baseUrl` only when you
are pointing at your own deployment.

### With curl

Three calls: upload the bytes, create the transcript, poll until it settles.

```bash title="transcribe.sh"
API="https://stt-api.sandchest.com"

# 1. Upload raw bytes. This is not a multipart form.
UPLOAD_URL=$(curl -sS "$API/v2/upload" \
  -H "Authorization: $SANDCHEST_API_KEY" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @meeting.mp3 | jq -r .upload_url)

# 2. Create the transcript.
ID=$(curl -sS "$API/v2/transcript" \
  -H "Authorization: $SANDCHEST_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"audio_url\":\"$UPLOAD_URL\"}" | jq -r .id)

# 3. Poll. GET returns the current state immediately, so you drive the loop.
while true; do
  STATUS=$(curl -sS "$API/v2/transcript/$ID" \
    -H "Authorization: $SANDCHEST_API_KEY" | jq -r .status)
  echo "$STATUS"
  [ "$STATUS" = completed ] || [ "$STATUS" = error ] && break
  sleep 2
done
```

> [!TIP]
> `audio_url` also accepts any public HTTPS URL, so you can skip step 1 entirely if
> your audio is already hosted somewhere reachable.

## 4. Read the words

Every word carries `start` and `end` in **milliseconds** and a `confidence` between
0 and 1.

```bash
curl -sS "$API/v2/transcript/$ID" \
  -H "Authorization: $SANDCHEST_API_KEY" \
  | jq -r '.words[] | "\(.start)\t\(.end)\t\(.text)"' | head
```

```text title="expected output"
0	280	Okay
300	520	so,
980	1090	the
1100	1480	deploy
1500	1690	went
```

The full transcript object also carries `text`, `confidence`, `audio_duration` (in
**seconds**), `language_code`, `language_confidence` and `speech_model_used`. See
[Transcript options](/docs/transcripts) for every field.

## Verify

Run this. It should print `completed` and a non-empty transcript id.

```bash
curl -sS "https://stt-api.sandchest.com/v2/transcript/$ID" \
  -H "Authorization: $SANDCHEST_API_KEY" \
  | jq '{id, status, language_code, audio_duration, speech_model_used, words: (.words | length)}'
```

```json title="expected output"
{
  "id": "tr_...",
  "status": "completed",
  "language_code": "en",
  "audio_duration": 4.8,
  "speech_model_used": "nvidia/parakeet-tdt-0.6b-v2",
  "words": 14
}
```

If `status` is `error`, read the `error` field — it says exactly what went wrong.
[Errors and limits](/docs/errors) maps every failure to what to do about it.

## Next

[Set up with an AI agent](/docs/agents) — hand these docs to a coding agent and let
it wire Sandchest in for you.
