---
name: typestream-voice-dictation
description: >-
  Add voice dictation (speech-to-text) to any web app with Typestream.
  Use when the user wants microphone input, voice typing, transcription,
  or speech-to-text. The user only needs to provide a Typestream API key.
---

# Typestream Voice Dictation

Integrate low-latency speech-to-text into the user's app. **The only secret the user must supply is a Typestream API key** (`ts_live_...`). Never put that key in client-side code.

## Before you start

1. **Read the project** — detect framework (Next.js App Router, Pages Router, React SPA, Vue, Svelte, plain HTML, Python/FastAPI backend, etc.) and match existing patterns for API routes, env vars, and styling.
2. **Ask the user for their API key** if they have not provided one. They can create one at https://app.typestream.dev/dashboard (30 free credits on signup).
3. **Store the key server-side only** — e.g. `TYPESTREAM_API_KEY` in `.env` / `.env.local`. Never commit it.

## Architecture (required)

```
Browser  →  your proxy (/api/typestream/...)  →  https://api.typestream.dev
```

The browser calls **your** backend. Your backend attaches `Authorization: Bearer <API_KEY>` and forwards to Typestream. The `@typestream/recorder` package handles mic capture, upload, and UI state; it only needs a `proxyUrl` pointing at your proxy.

## Step 1 — Environment variables

Add to the project's server env (not `NEXT_PUBLIC_*`):

```bash
TYPESTREAM_API_KEY=ts_live_...   # from the user
TYPESTREAM_API_URL=https://api.typestream.dev
```

Use `TYPESTREAM_API_URL` (or `API_URL`) consistently in proxy code.

## Step 2 — Backend proxy

Expose these routes from the **same origin** as the frontend (typical path prefix: `/api/typestream`):

| Method | Path | Forwards to |
|--------|------|-------------|
| `POST` | `/api/typestream/transcribe` | `POST /transcribe` (multipart `file`) |
| `POST` | `/api/typestream/sessions` | `POST /sessions` (JSON `{ "content_type": "audio/webm" }`) |
| `POST` | `/api/typestream/sessions/:id/complete` | `POST /sessions/:id/complete` |

Audio under **120 seconds** uses `/transcribe`. Longer recordings use the session + presigned upload flow (the recorder package handles this automatically).

### Next.js App Router example

```ts
// app/api/typestream/transcribe/route.ts
const API_URL = process.env.TYPESTREAM_API_URL ?? "https://api.typestream.dev";
const API_KEY = process.env.TYPESTREAM_API_KEY!;

export async function POST(request: Request) {
  const formData = await request.formData();
  const res = await fetch(`${API_URL}/transcribe`, {
    method: "POST",
    headers: { Authorization: `Bearer ${API_KEY}` },
    body: formData,
  });
  return Response.json(await res.json(), { status: res.status });
}
```

```ts
// app/api/typestream/sessions/route.ts
export async function POST(request: Request) {
  const body = await request.json();
  const res = await fetch(`${API_URL}/sessions`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });
  return Response.json(await res.json(), { status: res.status });
}
```

```ts
// app/api/typestream/sessions/[id]/complete/route.ts
export async function POST(
  _req: Request,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;
  const res = await fetch(`${API_URL}/sessions/${id}/complete`, {
    method: "POST",
    headers: { Authorization: `Bearer ${API_KEY}` },
  });
  return Response.json(await res.json(), { status: res.status });
}
```

Adapt route file layout for Pages Router (`pages/api/...`), Express, FastAPI, etc. using the same three endpoints.

### FastAPI example (snippet)

```python
@app.post("/api/typestream/transcribe")
async def transcribe_proxy(file: UploadFile = File(...)):
    async with httpx.AsyncClient() as client:
        res = await client.post(
            f"{TYPESTREAM_URL}/transcribe",
            headers={"Authorization": f"Bearer {API_KEY}"},
            files={"file": (file.filename, await file.read())},
        )
    return res.json()
```

## Step 3 — Install the recorder package

```bash
npm install @typestream/recorder framer-motion lucide-react
```

`framer-motion` and `lucide-react` are peer dependencies for the styled React component. For headless or vanilla usage, only `@typestream/recorder` is required.

If using Next.js, add to `next.config`:

```ts
transpilePackages: ["@typestream/recorder"],
```

## Step 4 — Add UI (pick one)

Choose the option that best fits the app. **Match the app's design system** when using the headless hook.

### A. Drop-in component (fastest)

```tsx
"use client";

import { TypestreamRecorder } from "@typestream/recorder";

export function VoiceInput() {
  return <TypestreamRecorder proxyUrl="/api/typestream" />;
}
```

Wire the transcript into the user's target field (textarea, chat input, form) if they specified one.

### B. Headless hook (custom UI)

```tsx
"use client";

import { useTypestream } from "@typestream/recorder";

export function MyVoiceField() {
  const { state, transcript, volumeLevel, start, stop, reset } =
    useTypestream({ proxyUrl: "/api/typestream" });

  // On state === "completed", merge `transcript` into your input state, then reset().
  // state: "idle" | "recording" | "uploading" | "processing" | "completed" | "error"
}
```

Typical pattern for a text input: when `state === "completed"` and `transcript` is set, append it to the input value and call `reset()`.

### C. Vanilla JS

```ts
import { createTypestream } from "@typestream/recorder/vanilla";

const recorder = createTypestream({
  proxyUrl: "/api/typestream",
  onTranscript: (text) => { /* update DOM or callback */ },
});
```

### D. Web component (no React)

```html
<script type="module">
  import "@typestream/recorder/web-component";
</script>
<typestream-recorder proxy-url="/api/typestream"></typestream-recorder>
```

## Step 5 — Verify

- [ ] API key is in server env only
- [ ] Proxy routes return 401-style errors if key is missing (do not leak the key in error messages)
- [ ] Mic permission prompt appears on record
- [ ] Short recording returns `{ transcription, duration, credits }`
- [ ] Transcript appears in the intended UI element

## API reference

- Docs: https://app.typestream.dev/docs
- OpenAPI: https://api.typestream.dev/docs
- Marketing / pricing: https://typestream.dev

## Errors

| Status | Meaning |
|--------|---------|
| 401 | Missing or invalid API key |
| 402 | Insufficient credits — user should top up at https://app.typestream.dev/dashboard |
| 415 | Unsupported audio MIME type |

## Rules

- **Never** expose `ts_live_...` in frontend code, git, or client env vars.
- **Always** proxy through the app's backend.
- Prefer the `@typestream/recorder` package over reimplementing MediaRecorder + upload logic.
- Integrate into the user's existing components and styles; do not bolt on a generic demo page unless asked.
