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

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

```bash
pip install pydantic-settings httpx
```

## 3. Connect

`pydantic-settings` reads `.env` and `.env.local` itself; if `app/config.py` already exists, add the fields to its `Settings`.

**`app/config.py`**

```python
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    # ExtraOrbital writes .env.local when the project has one, else .env; the later file wins.
    model_config = SettingsConfigDict(env_file=(".env", ".env.local"), extra="ignore")

    DEEPGRAM_BASE_URL: str
    DEEPGRAM_TOKEN_URL: str
    DEEPGRAM_REFRESH_TOKEN: str


settings = Settings()
```

**`app/speech.py`**

```python
import httpx

from app.config import settings


async def deepgram_token() -> str:
    """A 30-second token: ask for one right before each request or socket."""
    async with httpx.AsyncClient() as client:
        grant = await client.post(
            settings.DEEPGRAM_TOKEN_URL,
            headers={"Authorization": f"Bearer {settings.DEEPGRAM_REFRESH_TOKEN}"},
        )
        grant.raise_for_status()
        return grant.json()["access_token"]


async def transcribe_url(url: str) -> dict:
    token = await deepgram_token()
    async with httpx.AsyncClient(timeout=120) as client:
        response = await client.post(
            f"{settings.DEEPGRAM_BASE_URL}/v1/listen",
            params={"model": "nova-3", "diarize": "true", "smart_format": "true"},
            headers={"Authorization": f"Bearer {token}"},
            json={"url": url},
        )
        response.raise_for_status()
        return response.json()
```

## 4. Use it

**`app/api/deepgram.py`**

```python
from fastapi import APIRouter

from app.speech import transcribe_url

router = APIRouter()
# In app/main.py: from app.api import deepgram; app.include_router(deepgram.router)


@router.get("/api/deepgram")
async def check_deepgram():
    result = await transcribe_url("https://dpgr.am/spacewalk.wav")
    return {"transcript": result["results"]["channels"][0]["alternatives"][0]["transcript"]}
```

## 5. Verify

```bash
npx extraorbital list        # the resource is listed as active
fastapi dev app/main.py
curl http://localhost:8000/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.
- A blocking client (`boto3`, `stripe`) belongs in a plain `def` route, which FastAPI runs in a threadpool; in `async def` it stalls every other request.
- With `uv`, `uv add` the same packages instead of `pip install`.
- 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
