API Documentation

Everything you need to integrate with FernPod programmatically. Built for AI agents.

v0.2.0 Base URL: https://fernpod.com

LLM / Agent-friendly version

Plain text, optimized for AI consumption. Feed this to your agent.

https://fernpod.com/docs/llm.txt

Authentication

FernPod supports two authentication methods. All authenticated endpoints require one of these headers.

Bearer Token (JWT)

Obtained from /auth/login or /auth/register. Expires after 7 days.

Authorization: Bearer <jwt-token>

API Key

Created via /api-keys. Requires active subscription. Keys start with fpod_.

Authorization: ApiKey fpod_<key>

Auth Endpoints

POST /auth/register Public

Create a new account. Returns a JWT token.

Notes

  • email (string, required) — valid email address
  • password (string, required) — minimum 8 characters
  • name (string, optional) — display name

Request Body

{
  "email": "you@example.com",
  "password": "securepassword",
  "name": "Jane Podcaster"
}

Response

{
  "user": {
    "id": "a1b2c3d4e5f6a1b2c3d4",
    "email": "you@example.com",
    "name": "Jane Podcaster",
    "plan": "canceled"
  },
  "token": "eyJhbGciOiJIUzI1NiJ9..."
}
POST /auth/login Public

Authenticate and receive a JWT token.

Notes

  • email (string, required)
  • password (string, required)

Request Body

{
  "email": "you@example.com",
  "password": "securepassword"
}

Response

{
  "user": {
    "id": "a1b2c3d4e5f6a1b2c3d4",
    "email": "you@example.com",
    "name": "Jane Podcaster",
    "plan": "starter"
  },
  "token": "eyJhbGciOiJIUzI1NiJ9..."
}
GET /auth/me Auth Required

Get the current authenticated user's info.

Response

{
  "user": {
    "id": "a1b2c3d4e5f6a1b2c3d4",
    "email": "you@example.com",
    "name": "Jane Podcaster",
    "plan": "starter",
    "stripe_customer_id": "cus_xxx",
    "created_at": "2026-01-15T10:30:00Z"
  }
}

Podcasts

GET /podcasts Auth Required

List all podcasts owned by the authenticated user.

Response

{
  "podcasts": [
    {
      "id": "a1b2c3d4e5f6a1b2c3d4",
      "title": "My Great Podcast",
      "slug": "my-great-podcast",
      "description": "A show about things",
      "author": "Jane Podcaster",
      "language": "en",
      "category": "Technology",
      "image_url": "/covers/uid/pid/cover.jpg",
      "explicit": 0,
      "is_private": 0,
      "created_at": "2026-01-15T10:30:00Z",
      "updated_at": "2026-01-15T10:30:00Z"
    }
  ]
}
POST /podcasts Auth Required

Create a new podcast. Requires active subscription.

Notes

  • title (string, required) — podcast title
  • description (string, optional)
  • author (string, optional) — defaults to user name
  • email (string, optional) — defaults to user email
  • language (string, optional) — ISO code, defaults to "en"
  • category (string, optional) — iTunes category
  • subcategory (string, optional) — iTunes subcategory
  • explicit (boolean, optional) — defaults to false
  • is_private (boolean, optional) — defaults to false

Request Body

{
  "title": "My Great Podcast",
  "description": "A show about things",
  "author": "Jane Podcaster",
  "language": "en",
  "category": "Technology",
  "explicit": false
}

Response

{
  "id": "a1b2c3d4e5f6a1b2c3d4",
  "title": "My Great Podcast",
  "slug": "my-great-podcast",
  ...
}
GET /podcasts/:id Auth Required

Get a single podcast by ID.

Response

{
  "podcast": { ... }
}
PATCH /podcasts/:id Auth Required

Update a podcast. Send only the fields you want to change.

Notes

  • title, description, author, email, language, category, subcategory, image_url, website_url, explicit, is_private — all optional

Request Body

{
  "title": "Updated Title",
  "category": "Arts"
}

Response

{ ... updated podcast }
POST /podcasts/:id/cover Auth Required

Upload cover art. Send raw image bytes (not multipart form data). Max 10 MB. Supports PNG, JPG.

Notes

  • Request body: raw binary image data
  • Set Content-Type to the image MIME type

Response

{
  "image_url": "/covers/uid/pid/cover.jpg"
}
DELETE /podcasts/:id Auth Required

Delete a podcast and all its episodes, audio files, and analytics. This is permanent.

Response

{
  "deleted": true
}

Episodes

All episode routes are nested under /podcasts/:podcastId/episodes.

GET /podcasts/:podcastId/episodes Auth Required

List all episodes for a podcast. Optionally filter by status.

Notes

  • Query: ?status=draft|published|scheduled|unpublished|planned (optional)

Response

{
  "episodes": [
    {
      "id": "b2c3d4e5f6a1b2c3d4e5",
      "podcast_id": "a1b2c3d4e5f6a1b2c3d4",
      "title": "Episode 1: Getting Started",
      "slug": "episode-1-getting-started",
      "description": "The first episode",
      "status": "published",
      "audio_url": "/audio/uid/pid/eid.mp3",
      "audio_size": 15728640,
      "audio_duration": 1845,
      "audio_type": "audio/mpeg",
      "season": 1,
      "episode_number": 1,
      "episode_type": "full",
      "published_at": "2026-01-20T08:00:00Z"
    }
  ]
}
POST /podcasts/:podcastId/episodes Auth Required

Create a new episode (metadata only — upload audio or generate via TTS separately). Requires active subscription.

Notes

  • title (string, required)
  • description (string, optional)
  • season (number, optional)
  • episode_number (number, optional)
  • episode_type (string, optional) — "full", "trailer", or "bonus"
  • explicit (boolean, optional)
  • status (string, optional) — "draft" (default), "published", or "planned"
  • scheduled_at (string, optional) — ISO timestamp for scheduled publish
  • planned_for (string, optional) — date for content calendar
  • chapters (array, optional) — array of { start: number, title: string } for chapter markers

Request Body

{
  "title": "Episode 1: Getting Started",
  "description": "The first episode",
  "season": 1,
  "episode_number": 1,
  "episode_type": "full",
  "status": "draft",
  "chapters": [
    { "start": 0, "title": "Introduction" },
    { "start": 120, "title": "Main Topic" },
    { "start": 900, "title": "Wrap Up" }
  ]
}

Response

{ ... episode object }
POST /podcasts/:podcastId/episodes/:episodeId/upload Auth Required

Upload audio to an existing episode. Send raw audio bytes (not multipart). Supports MP3, M4A, OGG.

Notes

  • Request body: raw binary audio data
  • Set Content-Type to the audio MIME type

Response

{
  "episode": { ... updated episode with audio fields },
  "audio": {
    "size": 15728640,
    "duration": 1845,
    "type": "audio/mpeg",
    "tags": {
      "title": "Episode 1",
      "artist": "Jane Podcaster",
      "album": "My Podcast",
      "year": 2026,
      "duration": 1845
    }
  }
}
GET /podcasts/:podcastId/episodes/:episodeId Auth Required

Get a single episode by ID.

Response

{
  "episode": { ... }
}
PATCH /podcasts/:podcastId/episodes/:episodeId Auth Required

Update an episode. Send only the fields you want to change. Setting status to "published" auto-sets published_at.

Notes

  • title, description, season, episode_number, episode_type, explicit, status, scheduled_at, planned_for, chapters — all optional

Request Body

{
  "status": "published",
  "description": "Updated episode description",
  "chapters": [
    { "start": 0, "title": "Introduction" },
    { "start": 120, "title": "Main Topic" },
    { "start": 900, "title": "Wrap Up" }
  ]
}

Response

{ ... updated episode }
DELETE /podcasts/:podcastId/episodes/:episodeId Auth Required

Delete an episode and its audio file from storage. This is permanent.

Response

{
  "deleted": true
}

Text-to-Speech

Generate audio from text. The TTS endpoint accepts text and produces audio that is attached to an existing episode. This is the core of the agent workflow: create an episode, then generate audio from a script.

POST /podcasts/:podcastId/episodes/:episodeId/tts Auth Required

Generate audio from text for an existing episode. Audio generation is asynchronous — the endpoint returns immediately with a processing status.

Notes

  • text (string, required) — the text content to convert to speech
  • Returns 202 Accepted — audio generation happens asynchronously
  • Poll GET /podcasts/:podcastId/episodes/:episodeId to check when audio is ready (audio_url will be populated)

Request Body

{
  "text": "Welcome to episode one of My Great Podcast. Today we are going to talk about..."
}

Response

{
  "status": "processing",
  "episodeId": "b2c3d4e5f6a1b2c3d4e5"
}

Transcription

Trigger AI transcription on uploaded audio. Generates a full transcript with timestamps and auto-generates chapter markers.

POST /podcasts/:podcastId/episodes/:episodeId/transcribe Auth Required

Trigger AI transcription on an episode's audio. Requires audio to be uploaded first. Asynchronous — poll the episode to check transcript_status.

Notes

  • Episode must have uploaded audio (audio_url must be set)
  • Returns 202 Accepted — transcription runs asynchronously
  • Poll GET /podcasts/:podcastId/episodes/:episodeId and check transcript_status field
  • When complete, chapters are auto-generated and stored in ai_chapters

Response

{
  "status": "processing",
  "episodeId": "b2c3d4e5f6a1b2c3d4e5"
}
GET /podcasts/:podcastId/episodes/:episodeId/transcript Auth Required

Fetch the full transcript for an episode.

Notes

  • Returns 404 if no transcript exists
  • Returns 202 with { status: "processing" } if transcription is still running

Response

{
  "transcript": {
    "text": "Welcome to episode one...",
    "segments": [
      { "start": 0.0, "end": 3.5, "text": "Welcome to episode one" },
      { "start": 3.5, "end": 7.2, "text": "of My Great Podcast." }
    ]
  },
  "status": "completed"
}

Analytics

Full analytics with geographic, app, and referrer breakdowns included with all subscriptions.

GET /podcasts/:podcastId/analytics Auth Required

Get podcast-level analytics.

Notes

  • Query: ?days=30 (optional, default: 30)

Response

{
  "analytics": {
    "totalDownloads": 12450,
    "previousDownloads": 10200,
    "downloadGrowth": 22.1,
    "dailyDownloads": [
      { "date": "2026-01-15", "downloads": 450 }
    ],
    "topEpisodes": [
      { "id": "...", "title": "...", "downloads": 3200 }
    ],
    "byCountry": [
      { "country": "US", "downloads": 8000 }
    ],
    "byListeningApp": [
      { "app": "Apple Podcasts", "downloads": 5000 }
    ],
    "referrers": [
      { "referer": "twitter.com", "downloads": 1200 }
    ]
  }
}
GET /podcasts/:podcastId/analytics/episodes/:episodeId Auth Required

Get analytics for a single episode.

Notes

  • Query: ?days=30 (optional, default: 30)

Response

{
  "analytics": {
    "totalDownloads": 3200,
    "previousDownloads": 2800,
    "downloadGrowth": 14.3,
    "dailyDownloads": [ ... ],
    "byCountry": [ ... ],
    "byListeningApp": [ ... ],
    "referrers": [ ... ]
  }
}

Billing

POST /billing/checkout Auth Required

Create a Stripe Checkout session to start your subscription.

Notes

  • plan (string, required) — "starter"
  • Redirect the user to the returned URL

Request Body

{
  "plan": "starter"
}

Response

{
  "url": "https://checkout.stripe.com/c/pay/..."
}
POST /billing/portal Auth Required

Create a Stripe Customer Portal session to manage subscription.

Notes

  • Requires an existing Stripe subscription

Response

{
  "url": "https://billing.stripe.com/p/session/..."
}
GET /billing/status Auth Required

Get current billing status.

Response

{
  "plan": "starter",
  "stripe_customer_id": "cus_xxx",
  "stripe_subscription_id": "sub_xxx"
}

API Keys

Requires active subscription. API keys provide an alternative to JWT tokens for programmatic access.

GET /api-keys Auth Required

List all API keys for the authenticated user.

Response

{
  "api_keys": [
    {
      "id": "c3d4e5f6a1b2c3d4e5f6",
      "key_prefix": "fpod_a1b2",
      "name": "CI/CD Pipeline",
      "scopes": "read,write",
      "last_used_at": "2026-02-10T14:00:00Z",
      "created_at": "2026-01-15T10:30:00Z"
    }
  ]
}
POST /api-keys Auth Required

Create a new API key. The full key is only returned once — save it immediately.

Notes

  • name (string, required) — descriptive name
  • scopes (string, optional) — defaults to "read,write"

Request Body

{
  "name": "CI/CD Pipeline",
  "scopes": "read,write"
}

Response

{
  "api_key": {
    "id": "c3d4e5f6a1b2c3d4e5f6",
    "key": "fpod_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
    "prefix": "fpod_a1b2",
    "name": "CI/CD Pipeline",
    "scopes": "read,write"
  },
  "warning": "Save this API key now. It will not be shown again."
}
DELETE /api-keys/:id Auth Required

Revoke an API key. This is immediate and permanent.

Response

{
  "deleted": true
}

Feedback

POST /feedback Auth Required

Submit feedback (bug report, feature request, or general).

Notes

  • type (string, optional) — "bug", "feature", or "general" (default: "general")
  • subject (string, required) — max 200 characters
  • message (string, required) — max 5000 characters

Request Body

{
  "type": "feature",
  "subject": "Add video support",
  "message": "It would be great to support video podcasts..."
}

Response

{
  "feedback": {
    "id": "d4e5f6a1b2c3d4e5f6a1",
    "type": "feature",
    "subject": "Add video support",
    "message": "It would be great to support video podcasts...",
    "status": "open",
    "created_at": "2026-02-15T10:30:00Z"
  }
}
GET /feedback Auth Required

List all your feedback submissions.

Response

{
  "feedback": [ ... ]
}
GET /feedback/:id Auth Required

Get a single feedback item.

Response

{
  "feedback": { ... }
}

Public Content

These routes require no authentication and serve public-facing content.

GET /feed/:slug

RSS feed for a podcast. Apple Podcasts and Spotify compatible. Submit this URL to podcast directories.

GET /p/:slug

Public podcast website page with episode listings and embedded player.

GET /p/:slug/episodes/:episodeSlug

Public episode page with player, description, and metadata.

GET /embed/:episodeId

Embeddable episode player widget. Use in an iframe.

GET /embed/podcast/:podcastId

Embeddable podcast player showing the 5 most recent episodes.

GET /audio/:userId/:podcastId/:filename

Audio file streaming with range request support. Downloads are tracked and deduplicated (same IP + episode within 24h = 1 download).

GET /covers/:userId/:podcastId/:filename

Cover art image serving. Cached for 24 hours.

Plan Limits

Feature Starter ($10/mo)
PodcastsUnlimited
EpisodesUnlimited
StorageUnlimited
Downloads/monthUnlimited
AnalyticsFull
API accessYes
Text-to-SpeechYes
Custom domainYes
Private podcastsYes

7-day free trial included. Content goes offline when subscription is canceled. Fair use policy: 200,000 downloads/month soft cap.

Error Format

All errors return a JSON object with an error field and an appropriate HTTP status code.

{
  "error": "Description of what went wrong"
}
Status Meaning
400Bad request — missing or invalid parameters
401Unauthorized — missing or invalid auth token/key
403Forbidden — subscription required or insufficient permissions
404Not found — resource doesn't exist or not owned by you
429Rate limited — 60 requests per minute
500Server error — something went wrong on our end

Quick Start — Agent Workflow

Create an account, make a podcast, generate an episode with TTS, and publish — the full agent workflow:

# 1. Register
curl -X POST https://fernpod.com/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"securepass","name":"You"}'

# 2. Create a podcast (use the token from step 1)
curl -X POST https://fernpod.com/podcasts \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"My Podcast","description":"A great show"}'

# 3. Create an episode with chapters
curl -X POST https://fernpod.com/podcasts/PODCAST_ID/episodes \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Episode 1","status":"draft","chapters":[{"start":0,"title":"Intro"},{"start":120,"title":"Main Topic"}]}'

# 4a. Generate audio via TTS (agent workflow)
curl -X POST https://fernpod.com/podcasts/PODCAST_ID/episodes/EPISODE_ID/tts \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text":"Welcome to episode one..."}'

# 4b. OR upload audio directly
curl -X POST https://fernpod.com/podcasts/PODCAST_ID/episodes/EPISODE_ID/upload \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: audio/mpeg" \
  --data-binary @episode.mp3

# 5. Publish the episode
curl -X PATCH https://fernpod.com/podcasts/PODCAST_ID/episodes/EPISODE_ID \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"published"}'

Tips for AI Agents

  • 1
    TTS is async. POST to /tts returns 202 immediately. Poll the episode every 2–5 seconds until audio_url is set.
  • 2
    Transcription is async. POST to /transcribe returns 202. Poll episode for transcript_status = "completed".
  • 3
    Batch workflows. Create all episode metadata first, then trigger TTS/upload for each. Episodes can exist as drafts without audio.
  • 4
    Rate limit: 60 req/min. Space out batch operations. A 429 response means slow down.
  • 5
    Private podcasts. Set is_private: true on create. No public page, no RSS feed, no embed. Audio served via signed URLs. Learn more.
  • 6
    Auto-generated chapters. POST to /transcribe auto-generates chapter markers. Or include your own chapters array on episode create/update.