---
name: deepgram-express
description: "Provisions Deepgram speech-to-text with speaker diarization, through short-lived tokens for a Express app with `npx extraorbital add deepgram` (no signup, no card) and wires it into Express with `fetch`. Use when a Express 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 Express

<!-- 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 Express.

## 3. Connect

Node does not read env files by itself: start with `node --env-file=.env` (Node 20.6+), naming `.env.local` instead if that is where ExtraOrbital wrote, or load it with `dotenv`.

**`src/speech.js`**

```js
// 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) {
  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

**`src/app.js`**

```js
import { transcribeUrl } from "./speech.js";

// Express 5 sends a rejected promise to the error handler; on Express 4, wrap this in try/catch.
app.get("/api/deepgram", async (req, res) => {
  const result = await transcribeUrl("https://dpgr.am/spacewalk.wav");
  res.json({ transcript: result.results?.channels?.[0]?.alternatives?.[0]?.transcript });
});
```

## 5. Verify

```bash
npx extraorbital list        # the resource is listed as active
node --env-file=.env src/server.js
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.
- Snippets are ES modules (`"type": "module"`). In CommonJS, swap `import` for `require`.
- Create clients once at module scope, as here, not per request.
- 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
