Pepys

API

Transcription API

Transcribe any audio, video, or podcast programmatically – over the REST API, or as an MCP server so AI agents transcribe on their own. Async jobs with polling or webhooks, and native podcast (RSS / Apple) support.

MCP – give an AI agent Pepys

Want the short version? See the MCP server page. Full setup below.

Let an AI agent (Claude, ChatGPT, Cursor, and other MCP clients) transcribe on its own – hours-long files, diarization, correctly-timed SRT/VTT, paste-a-link, whole podcast feeds, and search across a transcript – over the Model Context Protocol. Two ways to connect:

1. Hosted connector – no API key

Point a remote MCP connector at https://pepys.co/api/mcp. The agent signs in to Pepys once (OAuth); transcription bills to your credits. In Claude: Settings → Connectors → Add custom connector. Best for Claude and ChatGPT.

2. Local server – your own key

Run the published pepys-mcp server with your API key – best for local dev agents. Add to your client's MCP config:

{
  "mcpServers": {
    "pepys": {
      "command": "npx",
      "args": ["-y", "pepys-mcp"],
      "env": { "PEPYS_API_KEY": "pk_live_your_key" }
    }
  }
}

Both expose the same tools – transcribe, get_transcription, upload_file, export_transcript, transcribe_podcast_feed, search_transcript, and get_credit_balance. The 60 free minutes on signup apply; diarization, whole-feed batch, and word-level export unlock with any one-time purchase.

Pepys is listed on Smithery, mcp.so, and the MCP Registry.

Authentication

Create a key under Settings → API keys (shown once). Send it as a bearer token. All endpoints are under https://pepys.co/api/v1.

curl https://pepys.co/api/v1/transcriptions \
  -H "Authorization: Bearer pk_live_your_key"

Usage draws from your account credit balance (1 credit = 1 minute). Enable auto-reload for unattended jobs so they don't pause at a zero balance.

Create a transcription

POST /api/v1/transcriptions

Pass exactly one source: a remote url (audio/video file, podcast RSS or Apple link, or a social link), a media_ref from a direct upload, or a public blob_url.

curl -X POST https://pepys.co/api/v1/transcriptions \
  -H "Authorization: Bearer pk_live_…" \
  -H "Idempotency-Key: my-unique-id" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/episode.mp3",
    "diarize": true,
    "summary": true,
    "language": "en"
  }'

# → 202 { "id": "…", "status": "queued", "url": "/api/v1/transcriptions/…" }

Options

  • language – ISO code; omit to auto-detect.
  • diarize – detect speakers (billed at 3× on long-form).
  • summary, chapters – AI extras.
  • translate_to – ISO code to also return a translation.
  • qualityfast | accurate.

Reuse an Idempotency-Key to make retries safe. A cached or caption-backed source returns 200 with status: "done" immediately.

Get a transcription

GET /api/v1/transcriptions/{id}

Poll until status is done (or failed), or use a webhook instead.

{
  "id": "…",
  "status": "done",
  "language": "en",
  "duration_seconds": 1771,
  "word_count": 4120,
  "billed_minutes": 30,
  "text": "Full transcript…",
  "summary": "…",
  "segments": [
    { "start": 0.0, "end": 4.2, "speaker": "Speaker 1", "text": "Welcome back." }
  ]
}

List with GET /api/v1/transcriptions.

Podcasts

A podcast is an RSS feed; each episode's audio is its <enclosure>. Pass a feed URL (or an Apple Podcasts link) as url to transcribe its latest episode, or list episodes first and transcribe a specific one's audio_url.

GET /api/v1/podcasts/episodes?feed={rss_url}&limit=50

curl "https://pepys.co/api/v1/podcasts/episodes?feed=https://feeds.simplecast.com/54nAGcIl&limit=3" \
  -H "Authorization: Bearer pk_live_…"

# → { "total": 2900, "returned": 3, "data": [ { "title", "guid", "published_at", "duration_seconds", "audio_url" } ] }

# transcribe a specific episode by guid (no need to pass the audio_url):
curl -X POST https://pepys.co/api/v1/transcriptions \
  -H "Authorization: Bearer pk_live_…" \
  -d '{ "url": "<feed_url>", "episode_guid": "<guid>" }'
# (or "episode_index": 0 for the newest, 1 for the next, …)

Transcribe a whole feed

Fan a feed out into one transcription per episode (grouped as a batch).

POST /api/v1/podcasts/transcribe

curl -X POST https://pepys.co/api/v1/podcasts/transcribe \
  -H "Authorization: Bearer pk_live_…" \
  -d '{ "feed": "<feed_or_apple_url>", "episodes": "all", "diarize": true }'
# episodes: "latest" (default) | "all" | <number>
# → 202 { "batch_id": "…", "total": 42, "transcriptions": [ { "id", "title", "guid", "url" } ] }

Each returned transcription id is polled (or webhook-notified) like any other.

Direct uploads

For your own files, get a presigned URL, PUT the bytes, then transcribe the returned media_ref.

POST /api/v1/uploads

# 1. handshake
curl -X POST https://pepys.co/api/v1/uploads \
  -H "Authorization: Bearer pk_live_…" \
  -d '{ "filename": "call.mp3", "content_type": "audio/mpeg", "bytes": 5242880 }'
# → { "upload_url": "https://…", "media_ref": "uploads/<you>/<uuid>.mp3", "expires_in": 600 }

# 2. upload the bytes (Content-Type + Content-Length must match)
curl -X PUT "<upload_url>" -H "Content-Type: audio/mpeg" --data-binary @call.mp3

# 3. transcribe it
curl -X POST https://pepys.co/api/v1/transcriptions \
  -H "Authorization: Bearer pk_live_…" \
  -d '{ "media_ref": "uploads/<you>/<uuid>.mp3" }'

Webhooks

Register an endpoint under Settings → Webhooks (or POST /api/v1/webhooks) to receive a signed POST on transcription.completed and transcription.failed – no polling.

POST https://your-server.com/webhooks/pepys
Pepys-Event-Type: transcription.completed
Pepys-Event-Id: <jobId>.done
Pepys-Signature: t=1718800000,v1=<hmac-sha256-hex>

{
  "id": "<jobId>.done",
  "type": "transcription.completed",
  "created": 1718800000,
  "data": { "transcription": { "id": "…", "status": "done", "url": "https://pepys.co/api/v1/transcriptions/…" } }
}

Verify the signature

Recompute HMAC-SHA256 over `{timestamp}.{rawBody}` with your endpoint secret and constant-time compare against the v1 value.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const ok = v1.length === expected.length &&
    timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
  if (!ok) throw new Error("bad signature");
  // Optional: reject if Math.abs(Date.now()/1000 - Number(t)) > 300 (replay window).
  return JSON.parse(rawBody);
}

Respond 2xx within 10s. Non-2xx / timeouts retry with backoff (up to 6 attempts). The payload carries a summary; fetch the full transcript via GET /api/v1/transcriptions/{id}.

Errors & limits

  • 401 – missing/invalid key.
  • 402 – out of credits (top up / enable auto-reload).
  • 400 / 422 – bad params or an unresolvable link.
  • 429 – rate limited (a Retry-After header is included).

Clips for social are capped at 90s; transcription length follows your plan limits.