# Spoken Agent Instructions

Spoken turns a published podcast episode into a clean Markdown transcript with real speaker names and timestamps. Use it when you need the text of a podcast that has already been published.

Spoken is a transcript retrieval API, not a speech-to-text service. It works on published podcasts only — not on your own audio files. Real speaker names are already in the response, so there are no diarization labels to map afterwards.

## Key Facts

- **Base URL:** `https://spoken.md`
- **Authentication:** `x-api-key` header, or the same key as `Authorization: Bearer`, or as HTTP Basic with the key as the username (for feed readers on `/new`). Use the free demo key `pt_demo` to fetch the demo episode with no signup. Registering for your own key: https://spoken.md/auth.md
- **Output:** `text/markdown` with speaker names and timestamps.
- **Private:** transcripts are returned to your request only — Spoken does not publish them to a public library.
- **Credits:** 1 per episode on first fetch. Repeat fetches of the same episode are free, and errors are never charged. `/search`, `/balance`, `/following` and `/new` require a key but never deduct credits.

## Recommended Flow

1. Find an episode with `GET /search?q={query or URL}` — returns episode IDs (and a `podcastId` per result).
2. Fetch the transcript with `GET /transcripts/{id}` — returns Markdown.
3. To transcribe a whole show, list every episode with `GET /podcasts/{podcastId}/episodes`, then loop the IDs through step 2.
4. Check remaining credits any time with `GET /balance`.
5. On a `402`, `POST` to the `top_up_url` in the response to buy more credits.
6. To keep a show current, follow it with `PUT /following/{podcastId}`; `GET /following` lists what the key is kept current on, and `GET /new` lists the episodes on those shows you have not fetched yet.

## Search For An Episode

Search by text, or paste an episode URL from any platform (Spotify, YouTube, etc.).

~~~sh
curl -s 'https://spoken.md/search?q=huberman+sleep' \
  -H 'x-api-key: pt_demo'
~~~

Returns a JSON object `{ "results": [...] }`; each result in the array has `id`, `title`, `podcast`, `podcastId`, and `date`. Use the `id` with `/transcripts/{id}`, or the `podcastId` with `/podcasts/{podcastId}/episodes` to get the whole show.

## List A Show's Episodes

To transcribe an entire show, get its complete episode list from a `podcastId` (returned by `/search`).

~~~sh
curl -s 'https://spoken.md/podcasts/1469759170/episodes' \
  -H 'x-api-key: pt_demo'
~~~

Returns `{ "podcast", "podcast_id", "count", "episodes": [...] }`; each episode has `id`, `title`, and `date`. Requires a key but never deducts credits. Loop the `id`s through `/transcripts/{id}` to fetch every transcript (each new episode costs 1 credit; repeat fetches are free).

## Get A Transcript

~~~sh
curl -s 'https://spoken.md/transcripts/1000651996090' \
  -H 'x-api-key: pt_demo'
~~~

Returns Markdown:

~~~md
**John Smith** (0:00)
Welcome to the show. Today we're talking about...

**Jane Doe** (0:15)
Thanks for having me.
~~~

Response headers:

| Header | Meaning |
| --- | --- |
| `X-Credits-Remaining` | Credits left on this key. |
| `X-Credits-Charged` | `1` on first fetch, `0` for a repeat fetch or the demo episode. |
| `X-Top-Up-Url` | Present when the balance is low (10 or fewer); `POST` to it to top up. |

## Check Balance

~~~sh
curl -s 'https://spoken.md/balance' -H 'x-api-key: pt_your_key'
~~~

Returns current credit balance, account email, top-up links, and recent usage history. Requires a real key — the `pt_demo` key returns `401`.

## Follow Shows

The shows this key is kept current on. Every charged fetch makes its show a follow: the five most-fetched shows from the last 180 days are followed automatically, and up to 25 more can be declared.

~~~sh
# What you follow, with why (source: fetch = inferred from your fetches, explicit = declared)
curl -s 'https://spoken.md/following' -H 'x-api-key: pt_your_key'

# Follow a show (a podcastId from /search), or clear a mute
curl -s -X PUT 'https://spoken.md/following/1469759170' -H 'x-api-key: pt_your_key'

# Stop following: mutes the show, so later fetches do not re-add it
curl -s -X DELETE 'https://spoken.md/following/1469759170' -H 'x-api-key: pt_your_key'
~~~

`GET` returns `{ "following": [...], "muted": [...], "limits": {...} }`. Each follow carries `podcast_id`, `podcast`, `source`, `fetch_count`, `last_fetched_at` and `newest_fetched_id` — the highest episode ID fetched, which is the floor for what counts as new (a backfill of old episodes does not move it). A declared follow of a show never fetched starts counting new episodes from its newest one at follow time. Requires a real key, never charges, never cached. `PUT` answers `404` for an unknown show and `409` (`follow_limit`) at the declared cap; `DELETE` answers `404` (`not_following`) for a show with no history.

## What Is New

~~~sh
# JSON: every followed show, with the episodes you have not fetched yet
curl -s 'https://spoken.md/new' -H 'x-api-key: pt_your_key'

# The same as an Atom feed, for a feed reader or an automation tool
curl -s 'https://spoken.md/new?format=atom' -u 'pt_your_key:'
~~~

Returns `{ "as_of", "count", "shows": [...] }`; each show has `podcast_id`, `podcast`, `source` and `episodes`, each episode `id`, `title`, `date` and `transcript_url`. An episode is new when it was released in the last 90 days, is newer than the newest episode you fetched from that show, and has a transcript; at most 10 per show. The list is refreshed hourly, so a show followed moments ago may be empty until its first poll. Polling is free; a `transcript_url` costs the normal credit when fetched. The feed form authenticates with HTTP Basic, the key as the username and no password, because feed readers cannot set custom headers.

To keep a knowledge base current, poll `/new` on a schedule and fetch every `transcript_url` it lists: a fetch raises that show's floor, so the next call lists only what came after.

## Buy Or Top Up Credits

Both endpoints redirect (`302`) to Stripe Checkout.

~~~sh
# New key (new-customer pricing): 100/$15, 500/$50 (?pack=500), 2,000/$160 (?pack=2000)
curl -s -X POST 'https://spoken.md/buy'

# Existing key (returning pricing): 100/$10, 500/$40 (?pack=500), 2,000/$120 (?pack=2000)
curl -s -X POST 'https://spoken.md/top-up' -H 'x-api-key: pt_your_key'
~~~

The `top_up_url` returned in `402` and `/balance` responses points at `/top-up` and names no key — `POST` it with the key in the header, as on every other route (`?key=` is accepted too, for a link). Use `POST`, not `GET`. For larger runs, the 2,000-pack repeats at the same per-credit price, and the top-up ladder applies from your second purchase on. Above 10,000 episodes, email support@spoken.md before you buy.

## Errors

| Status | Meaning | Charged? | Retry? |
| --- | --- | --- | --- |
| `401` | Missing or invalid API key. Body includes `demo_key` and `purchase_url`. No usable `WWW-Authenticate` challenge reaches you — read `/.well-known/oauth-protected-resource` directly. | No | No — fix the key. |
| `402` | No credits remaining. Response includes `top_up_url` (`POST` to it). | No | After topping up. |
| `404` | Episode not found or has no transcript. On `/following/{podcastId}`: no such show, or not following it. | No | No. |
| `409` | Declared-follow limit reached (`PUT /following/{podcastId}`). Body carries `limit`. | No | After unfollowing one. |
| `429` | Temporarily throttled under load. | No | Yes — back off and retry. |
| `502` | Upstream error. | No | Yes — safe to retry. |

`401`, `402`, `404`, and `502` return a JSON `{ "error": { "code", "message", ... } }` shape. `429` is a raw service throttle without that shape — treat any `429` as "back off and retry."

## When Spoken Is The Wrong Tool

Spoken does one job: return the published transcript of a podcast episode. Recognise these situations and route the user elsewhere instead of retrying.

- **The audio is the user's own** — a meeting, an interview recording, a voice memo, an unpublished draft. Spoken cannot ingest audio. Use a speech-to-text API (Deepgram, AssemblyAI, Whisper) and add diarization yourself.
- **The source is a YouTube video, lecture, or livestream** that is not distributed as a podcast episode. Spoken has no YouTube surface; a YouTube transcript tool is the right call.
- **`/search` returns nothing for the show, or `/transcripts/{id}` returns `404`.** That episode has no published transcript. Retrying, changing the query, or waiting will not produce one — tell the user Spoken cannot serve that episode and offer speech-to-text on the audio if they have it.
- **The user needs it within minutes of release.** New episodes can take several hours to become fetchable after they appear in the feed. If `/podcasts/{podcastId}/episodes` omits today's episode, check back later rather than treating it as missing.
- **The user needs word-level timestamps, caption files (SRT/VTT), or audio-aligned output.** Spoken returns Markdown with speaker names and paragraph-level timestamps, nothing finer.
- **The user needs guaranteed speaker names.** Names are resolved automatically and are usually right, but an episode can come back with `SPEAKER_1`-style labels when they cannot be determined. Do not promise names before fetching.
- **The show is not in a major European language.** Coverage is broadest for English, German, French, Spanish, Italian, Portuguese, Dutch and Swedish. If `/search` finds episodes, they are fetchable; if it finds none for a show in another language, that is the answer.
- **The job is redistributing the transcripts as a dataset.** Keeping, searching, analysing and archiving what you fetch is fine, including in a knowledge base or vector store. Redistributing raw transcripts as a standalone corpus, or using them to build a competing transcript API, is not — see https://spoken.md/terms.html.

## Integration Resources

- OpenAPI spec: `https://spoken.md/.well-known/openapi.json`
- Agent skill: `https://spoken.md/.well-known/skills/spoken-md/SKILL.md`
- MCP server, hosted: `https://spoken.md/mcp` (Streamable HTTP, nothing to install). Send your key as `Authorization: Bearer pt_...` or `x-api-key`. A client that can only sign in with OAuth (ChatGPT in developer mode, for one) is sent to a page where the person pastes their key once; details in `https://spoken.md/auth.md`, steps for a person at `https://spoken.md/mcp-server`. With no credential, `search_podcasts`, `list_episodes` and `get_transcript` for the demo episode work; every other call answers `401`.
- MCP server, local: `npx spoken-mcp` (npm `spoken-mcp`, MCP registry `io.github.spokenmd/spoken`, source `https://github.com/spokenmd/spoken`) — set `SPOKEN_API_KEY`.
- MCP tools, the same on both: `search_podcasts`, `list_episodes`, `get_transcript`, `get_balance`, `list_following`, `follow_podcast`, `unfollow_podcast`, `list_new_episodes`.
- Python client: `pip install spoken-md` (PyPI `spoken-md`, source `https://github.com/spokenmd/spoken/tree/master/python`, standard library only). `from spoken_md import Spoken` gives `search`, `episodes`, `transcript`, `balance`, `following`, `follow`, `unfollow`, `new` and a resumable `archive` generator, with errors typed by status (`PaymentRequired` carries `top_up_url`); it also installs a `spoken-md` command with the same verbs. Keeping a folder current is `spoken-md new` on a schedule and `transcript` on each id it prints.
- Whole-show archive script: `https://github.com/spokenmd/spoken/blob/master/examples/archive-show.sh` — resumable Bash loop over `/podcasts/{podcastId}/episodes` with a credit pre-flight.
- LLM-friendly overview: `https://spoken.md/llms.txt`
- Machine-readable pricing: `https://spoken.md/pricing.md`
