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.quality–fast|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 (aRetry-Afterheader is included).
Clips for social are capped at 90s; transcription length follows your plan limits.