Pepys

API

Transcription API

Transcribe any audio, video, or podcast programmatically. One endpoint, async jobs with polling or webhooks, and native podcast (RSS / Apple) support.

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 urlto 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.