---
name: deepgram-nextjs
description: "Provisions Deepgram speech-to-text with speaker diarization, through short-lived tokens for a Next.js app with `npx extraorbital add deepgram` (no signup, no card) and wires it into Next.js with `fetch`. Use when a Next.js project needs Speech-to-text (Deepgram), transcription, speech recognition, voice, diarization or audio, or when DEEPGRAM_BASE_URL is missing or unset in .env."
---

# Speech-to-text (Deepgram) in Next.js

<!-- Generated from ExtraOrbital's service registry by `pnpm skills:build` in platform/app. Edit the generator, not this file. -->

ExtraOrbital provisions Deepgram speech-to-text with speaker diarization, through short-lived tokens in one command and writes the credentials into the project's env file. For anything this recipe does not cover, use the `extraorbital` skill or https://extraorbital.dev/llms.txt.

## 1. Provision

```bash
npx extraorbital add deepgram
```

Run it from the project root. It is idempotent (a second run returns the same resource) and writes to `.env.local` if the project has one, otherwise `.env`, keeping that file out of git. Add `--json` to parse the result; credential values are redacted there, and you should never print them either.

| Variable | |
| --- | --- |
| `DEEPGRAM_BASE_URL` | Deepgram's API origin |
| `DEEPGRAM_TOKEN_URL` | Exchanges the refresh credential for a 30-second token |
| `DEEPGRAM_REFRESH_TOKEN` | Stable credential: backend only |

Free on the Prototype plan: Uses your shared $5 credit; no separate free allowance.

## 2. Install the client

Nothing to install: `fetch` is built into Next.js.

## 3. Connect

Next.js loads both `.env.local` and `.env`, so either file ExtraOrbital writes is read on the next `next dev`.

**`lib/speech.ts`**

```ts
// A 30-second token: ask for one right before each request or socket.
export async function deepgramToken() {
  const grant = await fetch(process.env.DEEPGRAM_TOKEN_URL!, {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.DEEPGRAM_REFRESH_TOKEN!}` },
  });
  if (!grant.ok) throw new Error(`Deepgram token refused: ${grant.status}`);
  const { access_token } = await grant.json();
  return access_token;
}

export async function transcribeUrl(url: string) {
  const query = new URLSearchParams({ model: "nova-3", diarize: "true", smart_format: "true" });
  const response = await fetch(`${process.env.DEEPGRAM_BASE_URL!}/v1/listen?${query}`, {
    method: "POST",
    headers: { Authorization: `Bearer ${await deepgramToken()}`, "Content-Type": "application/json" },
    body: JSON.stringify({ url }),
  });
  if (!response.ok) throw new Error(`Transcription failed: ${response.status}`);
  return response.json();
}
```

## 4. Use it

**`app/api/deepgram/route.ts`**

```ts
import { transcribeUrl } from "@/lib/speech";

export async function GET() {
  const result = await transcribeUrl("https://dpgr.am/spacewalk.wav");
  return Response.json({ transcript: result.results?.channels?.[0]?.alternatives?.[0]?.transcript });
}
```

## 5. Verify

```bash
npx extraorbital list        # the resource is listed as active
npm run dev
curl http://localhost:3000/api/deepgram
```

A JSON answer means the credentials, the client and the route all work. Delete the route afterwards if the app does not need it.

## Pitfalls

- From the catalog: Exchange the refresh credential for a 30-second token, then connect directly to Deepgram. Transcription and speaker diarization spend the same shared credit as other services. Usage is polled every minute, subject to provider reporting delays. Exhausted credit stops new tokens; a top-up restores access with the same refresh credential. Existing streams are not disconnected.
- Grants are limited to one per resource every five seconds: reuse a token for requests that start within its 30 seconds instead of minting one per call.
- A browser or mobile client may receive a 30-second token to stream audio straight to Deepgram over WebSocket; it must never receive `DEEPGRAM_REFRESH_TOKEN`.
- Out of credit, the token endpoint answers 402; streams already open are not cut.
- Import the module only from server code (route handlers, server components, server actions). Only `NEXT_PUBLIC_*` variables reach the browser, and nothing else should.
- With a `src/` directory the module goes in `src/lib/`; without the `@/*` path alias, import it relatively.
- Restart `next dev` after the env file changes: it is read at startup.
- Exit code 3 means the account is past its free allowance: show the human the link the command printed and wait. Do not retry or work around it.

Docs: https://extraorbital.dev/docs/resources/deepgram.md
