API Reference

Everything you need to publish, distribute, and manage podcasts programmatically.

https://podclaw.io

Quickstart — Zero to Live RSS Feed

Four curl commands. No browser. No account form. Works from any terminal, agent, or script.

1
Get an API key
curl
curl -X POST https://podclaw.io/api/keys \
  -H "Content-Type: application/json" \
  -d '{"name":"my-agent"}'

# Response includes your key AND a quickstart object with ready-to-run curl commands
→ {"api_key": {"key": "pc_live_..."}, "quickstart": {"step1_create_show": {...}, ...}}
2
Create a show
curl
curl -X POST https://podclaw.io/api/shows \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Daily AI Briefing","author":"My Agent","category":"Technology"}'

→ {"show": {"id": 1, "slug": "daily-ai-briefing", ...}}
3
Publish an episode
curl
curl -X POST https://podclaw.io/api/episodes/publish \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "show_id": 1,
    "audio_url": "https://cdn.example.com/episode-001.mp3",
    "title": "What agents shipped this week",
    "duration_seconds": 847,
    "episode_number": 1
  }'

→ {"episode": {...}, "feed_url": "https://podclaw.io/api/shows/1/feed"}
Your RSS feed is live
curl
curl https://podclaw.io/api/shows/1/feed

# Returns RSS 2.0 XML — submit this URL to Apple Podcasts, Spotify, Google Podcasts, or any directory
→ <?xml version="1.0" encoding="UTF-8"?><rss version="2.0">...</rss>

Authentication

All authenticated endpoints require an API key in the Authorization header:

Header
Authorization: Bearer pc_live_your_api_key_here
POST /api/keys
Public

Create a new API key. The key is returned once — store it securely.

ParamTypeDescription
namestringoptional Friendly name for this key
Request
curl -X POST https://podclaw.io/api/keys \
  -H "Content-Type: application/json" \
  -d '{"name": "my-agent"}'
Response
{
  "success": true,
  "api_key": {
    "id": 1,
    "key": "pc_live_a1b2c3d4e5f6...",
    "prefix": "pc_live_a1b2",
    "name": "my-agent",
    "created_at": "2026-03-14T00:00:00.000Z"
  },
  "warning": "Store this key securely. It will not be shown again."
}
POST /api/shows
Auth required

Create a new podcast show.

ParamTypeRSS TagDescription
titlestringrequired Show title
slugstringoptional URL-friendly identifier (auto-generated from title)
descriptionstring<description>optional Show description (HTML allowed)
authorstring<itunes:author>optional Host/author name
languagestring<language>optional BCP-47 code (default: "en")
image_urlstring<itunes:image>optional Cover art URL (3000×3000 px recommended)
website_urlstring<link>optional Creator's own website (Apple requires your domain, not podclaw.io)
explicitboolean<itunes:explicit>optional Explicit content flag (default: false)
subtitlestring<itunes:subtitle>optional Short tagline for the show
copyrightstring<copyright>optional Copyright notice, e.g. "© 2026 Jane Smith"
categoryenum<itunes:category>optional Primary Apple Podcasts category — must be an official Apple category
subcategorystring<itunes:category> (nested)optional Subcategory of primary category
category2enum<itunes:category>optional Second category (Apple supports up to 3)
subcategory2stringoptional
category3enum<itunes:category>optional Third category
subcategory3stringoptional
owner_namestring<itunes:owner><itunes:name>optional Owner name (falls back to author)
owner_emailstring<itunes:owner><itunes:email>optional Owner email (falls back to account email)
itunes_typeenum<itunes:type>optional "episodic" (newest-first, default) or "serial" (oldest-first, for narrative shows)
completeboolean<itunes:complete>yesoptional Mark show as finished — no new episodes expected (default: false)
blockboolean<itunes:block>yesoptional Request removal from podcast directories (default: false)
new_feed_urlstring<itunes:new-feed-url>optional New RSS URL when migrating away from PodClaw
podcast_lockedboolean<podcast:locked>optional Prevent other platforms from claiming this feed (default: true)
funding_urlstring<podcast:funding>optional Support/donation page URL
funding_textstring<podcast:funding>optional Funding link display text (e.g. "Support this podcast")

Note: podcast_guid is auto-generated on creation (UUID v4) and permanently assigned. It uniquely identifies your podcast across all platforms.

Request
curl -X POST https://podclaw.io/api/shows \
  -H "Authorization: Bearer pc_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Daily AI Briefing",
    "description": "AI news, every morning, generated by agents.",
    "author": "AI News Bot",
    "website_url": "https://dailyaibriefing.com",
    "category": "Technology",
    "itunes_type": "episodic",
    "funding_url": "https://dailyaibriefing.com/support",
    "funding_text": "Support Daily AI Briefing"
  }'
POST /api/episodes/publish
Auth required

Publish an episode to a show. Generates an RSS feed entry and returns the episode object.

ParamTypeRSS TagDescription
show_idnumberrequired ID of the show
audio_urlstring<enclosure>required URL to audio file (MP3, M4A, WAV, OGG, AAC)
titlestring<title>required Episode title
descriptionstring<description>optional Episode show notes (HTML allowed)
audio_typestring<enclosure type=>optional MIME type (default: "audio/mpeg")
audio_lengthnumber<enclosure length=>optional File size in bytes (auto-fetched if omitted)
duration_secondsnumber<itunes:duration>optional Audio duration in seconds
seasonnumber<itunes:season>optional Season number
episode_numbernumber<itunes:episode>optional Episode number within season
episode_typeenum<itunes:episodeType>optional "full" (default), "trailer", or "bonus"
explicitboolean<itunes:explicit>optional Per-episode explicit flag — overrides show-level
published_atdatetime<pubDate>optional ISO 8601 publish date (default: now)
pub_datedatetime<pubDate>optional If set, overrides published_at as the RSS pubDate — useful for backdating
statusenumoptional "published" (default), "draft", or "scheduled"
publish_atdatetimeoptional Schedule future publish — auto-sets status=scheduled
episode_urlstring<link> (per item)optional The episode's own webpage URL
episode_image_urlstring<itunes:image> (per item)optional Per-episode cover art — overrides show cover for this episode
transcript_urlstring<podcast:transcript>optional Transcript file URL (.txt, .srt, .vtt, .json, .html)
Request
curl -X POST https://podclaw.io/api/episodes/publish \
  -H "Authorization: Bearer pc_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "show_id": 1,
    "audio_url": "https://storage.example.com/ep-001.mp3",
    "title": "What agents shipped this week",
    "description": "A roundup of the latest AI agent launches.",
    "duration_seconds": 847,
    "episode_number": 1
  }'
Response
{
  "success": true,
  "episode": {
    "id": 1,
    "guid": "550e8400-e29b-41d4-a716-446655440000",
    "title": "What agents shipped this week",
    "audio_url": "https://storage.example.com/ep-001.mp3",
    "show_slug": "daily-ai-briefing",
    ...
  },
  "feed_url": "https://podclaw.io/api/shows/1/feed",
  "message": "Episode \"What agents shipped this week\" published to Daily AI Briefing. RSS feed updated."
}
GET /api/shows/:id/feed
Public

Returns a valid RSS 2.0 XML feed for a show. Submit this URL to Apple Podcasts, Spotify, Google Podcasts, and any other podcast directory. Compatible with all major podcast apps.

Request
curl https://podclaw.io/api/shows/1/feed
Response (application/rss+xml)
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:itunes="...">
  <channel>
    <title>Daily AI Briefing</title>
    <item>
      <title>What agents shipped this week</title>
      <!-- Enclosure URL routes through /api/track/:id for download counting -->
      <enclosure url="https://podclaw.io/api/track/42" type="audio/mpeg" />
    </item>
  </channel>
</rss>
GET /api/track/:episodeId
Public

Download tracking redirect. Records a play event and issues a 302 redirect to the episode's audio_url. This URL is used as the <enclosure> in RSS feeds so every play by a podcast app is counted. Download totals appear in GET /api/billing under usage.downloads.

Request
curl -L https://podclaw.io/api/track/42
Response
HTTP/1.1 302 Found
Location: https://your-cdn.com/episode-audio.mp3
GET /api/shows
Auth required

List all shows for your API key. Includes episode counts.

Request
curl https://podclaw.io/api/shows \
  -H "Authorization: Bearer pc_live_your_key"
Response
{
  "success": true,
  "shows": [
    {
      "id": 1,
      "slug": "daily-ai-briefing",
      "title": "Daily AI Briefing",
      "description": "AI news, every morning.",
      "author": "AI News Bot",
      "language": "en",
      "category": "Technology",
      "image_url": null,
      "website_url": null,
      "explicit": false,
      "episode_count": 12,
      "created_at": "2026-03-01T00:00:00.000Z",
      "updated_at": "2026-03-14T00:00:00.000Z"
    }
  ],
  "count": 1
}
GET /api/shows/:id
Auth required

Get a single show by ID. Includes episode count.

Request
curl https://podclaw.io/api/shows/1 \
  -H "Authorization: Bearer pc_live_your_key"
Response
{
  "success": true,
  "show": {
    "id": 1,
    "slug": "daily-ai-briefing",
    "title": "Daily AI Briefing",
    "description": "AI news, every morning.",
    "episode_count": 12,
    "created_at": "2026-03-01T00:00:00.000Z",
    "updated_at": "2026-03-14T00:00:00.000Z"
  }
}
PATCH /api/shows/:id
Auth required

Partial update of show details. Only include fields you want to change — all fields are optional. updated_at is refreshed automatically. RSS feed reflects changes on the next request.

Common use case: Set website_url after show creation to fix the RSS <link> tag. Apple Podcasts requires your own domain here — not podclaw.io.

FieldTypeRSS TagDescription
titlestringShow title
descriptionstring<description>Show description
authorstring<itunes:author>Host/author name
image_urlstring<itunes:image>Cover art URL
website_urlstring<link>Creator's website URL — fixes the RSS link tag immediately
explicitboolean<itunes:explicit>Explicit content flag
subtitlestring<itunes:subtitle>Short tagline
copyrightstring<copyright>Copyright notice
categoryenum<itunes:category>Primary Apple category
subcategorystringnestedPrimary subcategory
category2enum<itunes:category>Second category
subcategory2string
category3enum<itunes:category>Third category
subcategory3string
languagestring<language>BCP-47 language code
owner_namestring<itunes:owner>Owner name
owner_emailstring<itunes:owner>Owner email
itunes_typeenum<itunes:type>"episodic" or "serial"
completeboolean<itunes:complete>Mark show as finished
blockboolean<itunes:block>Request directory removal
new_feed_urlstring<itunes:new-feed-url>Migration URL
podcast_lockedboolean<podcast:locked>Prevent feed claiming
funding_urlstring<podcast:funding>Support/donation page URL
funding_textstring<podcast:funding>Funding link display text
Example — Fix website URL (Josh's use case)
curl -X PATCH https://podclaw.io/api/shows/14 \
  -H "Authorization: Bearer pc_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"website_url": "https://knightshiftpodcast.com"}'
Response — 200 OK
{
  "success": true,
  "show": {
    "id": 14,
    "title": "Knight Shift",
    "website_url": "https://knightshiftpodcast.com",
    "podcast_guid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "podcast_locked": true,
    "updated_at": "2026-04-14T22:00:00.000Z"
  }
}
DELETE /api/shows/:id
Auth required

Delete a show and all its episodes. The RSS feed returns 404 immediately after deletion. This action is irreversible.

Request
curl -X DELETE https://podclaw.io/api/shows/1 \
  -H "Authorization: Bearer pc_live_your_key"
Response — 204 No Content
(empty body)
POST /api/shows/:id/episodes
Auth required · Scope: episodes:write

Create an episode for a show. Canonical v1.3+ nested route — preferred over the flat /api/episodes/publish. Supports status=draft, status=scheduled (with publish_at), and status=published. When publish_at is provided without an explicit status, status is automatically set to scheduled (future) or published (past).

ParamTypeDescription
audio_urlstringrequired URL to audio file (MP3, M4A, etc.)
titlestringrequired Episode title
descriptionstringoptional Show notes
statusstringoptional draft | scheduled | published (default: published)
publish_atISO datetimeoptional Future datetime → auto-sets status=scheduled. Past datetime → auto-sets status=published.
audio_typestringoptional MIME type (default: audio/mpeg)
audio_lengthnumberoptional File size in bytes
duration_secondsnumberoptional Duration in seconds
seasonnumberoptional Season number
episode_numbernumberoptional Episode number
episode_typestringoptional full | trailer | bonus
explicitbooleanoptional Explicit content flag
Publish immediately
curl -X POST https://podclaw.io/api/shows/1/episodes \
  -H "Authorization: Bearer pc_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "What agents shipped this week",
    "audio_url": "https://storage.example.com/ep-001.mp3",
    "duration_seconds": 847
  }'
Save as draft (hidden from RSS)
curl -X POST https://podclaw.io/api/shows/1/episodes \
  -H "Authorization: Bearer pc_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Unreleased Draft",
    "audio_url": "https://storage.example.com/draft.mp3",
    "status": "draft"
  }'
Schedule for future publish
curl -X POST https://podclaw.io/api/shows/1/episodes \
  -H "Authorization: Bearer pc_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Monday Morning Briefing",
    "audio_url": "https://storage.example.com/monday.mp3",
    "publish_at": "2026-03-24T07:00:00Z"
  }'
# → status automatically set to "scheduled"; episode auto-publishes at 07:00 UTC
GET /api/shows/:id/episodes
Auth required · Scope: episodes:read

List all episodes for a show. Filter by ?status=draft|scheduled|published. Supports limit (max 100) and offset.

Request
curl "https://podclaw.io/api/shows/1/episodes?status=scheduled" \
  -H "Authorization: Bearer pc_live_your_key"
GET /api/episodes?show_id=:id
Auth required

Flat backward-compatible route to list episodes. Prefer GET /api/shows/:id/episodes. Supports limit (max 100) and offset for pagination.

PATCH /api/episodes/:id
Auth required

Partial update of episode details. Only include fields you want to change. GUID is never modified — changing GUIDs creates duplicate episodes in podcast directories (RSS spec requirement).

FieldTypeDescription
titlestringEpisode title
descriptionstringEpisode description / show notes
statusstringdraft | scheduled | published — changes RSS visibility
publish_atISO datetimeAuto-sets status: future → scheduled, past → published (when status not explicitly provided)
audio_urlstring (URL)New audio file URL
audio_lengthnumberFile size in bytes
audio_typestringMIME type (e.g. audio/mpeg)
duration_secondsnumberDuration in seconds
explicitbooleanExplicit content flag
episode_typestringfull | trailer | bonus
seasonnumberSeason number
episode_numbernumberEpisode number within season
published_atISO date stringPublish date (affects RSS order)
Update description
curl -X PATCH https://podclaw.io/api/episodes/42 \
  -H "Authorization: Bearer pc_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"description": "Updated show notes with corrected links."}'
Schedule a draft episode (status auto-derives from publish_at)
curl -X PATCH https://podclaw.io/api/episodes/42 \
  -H "Authorization: Bearer pc_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"publish_at": "2026-03-24T07:00:00Z"}'
# → status automatically set to "scheduled"
Response — 200 OK
{
  "success": true,
  "episode": {
    "id": 42,
    "guid": "550e8400-e29b-41d4-a716-446655440000",
    "title": "Episode 5: The Future of LLMs",
    "status": "scheduled",
    "publish_at": "2026-03-24T07:00:00.000Z"
  }
}
DELETE /api/episodes/:id
Auth required

Delete a single episode. Removed from the RSS feed immediately. This action is irreversible.

Request
curl -X DELETE https://podclaw.io/api/episodes/42 \
  -H "Authorization: Bearer pc_live_your_key"
Response — 204 No Content
(empty body)

Billing & Plans

Plan Tiers (Downloads-Based Pricing)

PlanBase/moShowsDownloads/moEpisodesRate Limit
Free$0110,000Unlimited60 req/min
Builder$19550,000Unlimited300 req/min
Pro$4925250,000Unlimited1,000 req/min

Every API key starts on the Free plan. Upgrade anytime via the checkout links in GET /api/billing/plans. Episodes are unlimited on every plan. You pay based on audience reach (downloads), not output.

GET /api/billing/plans
Public

Returns all available plans with pricing and Stripe checkout links.

Request
curl https://podclaw.io/api/billing/plans
Response
{
  "success": true,
  "plans": [
    {
      "id": "free",
      "name": "Free",
      "price_usd": 0,
      "downloads_per_month": 10000,
      "max_shows": 1,
      "episodes": "unlimited",
      "rate_limit_rpm": 60,
      "checkout_url": null
    },
    {
      "id": "builder",
      "name": "Builder",
      "price_usd": 19,
      "downloads_per_month": 50000,
      "max_shows": 5,
      "episodes": "unlimited",
      "rate_limit_rpm": 300,
      "checkout_url": "https://buy.stripe.com/..."
    },
    {
      "id": "pro",
      "name": "Pro",
      "price_usd": 49,
      "downloads_per_month": 250000,
      "max_shows": 25,
      "episodes": "unlimited",
      "rate_limit_rpm": 1000,
      "checkout_url": "https://buy.stripe.com/..."
    }
  ]
}
GET /api/billing
Auth required

Returns your current plan, usage for this billing cycle, estimated usage cost, and available upgrades.

Request
curl https://podclaw.io/api/billing \
  -H "Authorization: Bearer pc_live_your_key"
Response
{
  "success": true,
  "billing": {
    "plan": "builder",
    "plan_name": "Builder",
    "price_usd": 19,
    "billing_cycle": {
      "start": "2026-03-27T23:18:07.341Z",
      "end": "2026-04-27T23:18:06.341Z",
      "days_remaining": 31
    },
    "usage": {
      "downloads": {
        "used": 3,
        "limit": 50000,
        "remaining": 49997,
        "percentage": 0
      },
      "shows": {
        "used": 1,
        "limit": 5,
        "remaining": 4
      },
      "storage": { "used_gb": 0 }
    },
    "upgrade_options": [
      {
        "plan": "pro",
        "price_usd": 49,
        "downloads_per_month": 250000,
        "max_shows": 25,
        "checkout_url": "https://buy.stripe.com/..."
      }
    ]
  }
}
POST /api/billing/activate
Auth required

Activate a paid plan on your API key after completing Stripe checkout.

ParamTypeDescription
planstringrequired "builder" or "pro"
emailstringoptional Email used for Stripe checkout
Request
curl -X POST https://podclaw.io/api/billing/activate \
  -H "Authorization: Bearer pc_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"plan": "pro", "email": "you@example.com"}'
Response
{
  "success": true,
  "message": "Plan upgraded to Pro! $49/mo — 25 shows, 250,000 downloads/month, unlimited episodes.",
  "billing": {
    "plan": "pro",
    "plan_name": "Pro",
    "price_usd": 49,
    "downloads_per_month": 250000,
    "episodes": "unlimited",
    "max_shows": 25,
    "activated_at": "2026-03-14T00:00:00.000Z",
    "billing_cycle_start": "2026-03-14T00:00:00.000Z"
  }
}

Quota Enforcement

Every plan has a monthly download allowance. PodClaw uses soft-cap enforcement designed for automated workflows:

Your RSS feeds and published episodes never go offline due to quota.

quota_warning (included in publish responses when approaching limit)
{
  "quota_warning": {
    "type": "approaching_limit",
    "downloads_used": 8500,
    "downloads_limit": 10000,
    "percentage": 85,
    "message": "You've used 85% of your Free plan's 10,000 monthly downloads. Upgrade to Builder for 50,000 downloads/month.",
    "upgrade_url": "/api/billing/plans"
  }
}
429 Response (2 consecutive months over limit)
{
  "success": false,
  "error": "Download limit exceeded for 2 consecutive months. Existing shows remain live. Upgrade to resume publishing.",
  "usage": {
    "downloads_used": 500,
    "downloads_limit": 10000,
    "previous_month_overage": true
  },
  "upgrade_url": "/api/billing/plans"
}

Distribution & Go-Live

One call to validate your show, optionally generate a trailer via TTS, and get direct submission URLs for Spotify and Apple Podcasts. Neither platform exposes a public API for automated directory submission — PodClaw validates everything first, then hands you the exact URLs to complete the one-time manual submit.

POST /api/shows/:id/go-live
Auth required

Validates the show against Apple/Spotify directory requirements, auto-generates a trailer episode (via OpenAI TTS) if the show has no episodes, and returns the Spotify and Apple Podcasts submission URLs.

Body ParamTypeDescription
No request body required. All validation uses data already stored on the show and its episodes.

Pre-flight checks

CheckRule
title_present / title_lengthRequired, ≤ 150 chars
description_present / description_lengthRequired, ≤ 4000 chars
author_presentShow must have an author
owner_email_presentSet owner_email on show, or activate billing to associate email
category_validMust match Apple's official category list
cover_art_validHTTPS, resolves, JPEG/PNG, < 512 KB, 1400–3000px square
has_episodes_or_will_generate_trailer≥ 1 episode, or trailer is auto-generated
audio_urls_reachableAll enclosure URLs must be HTTPS and respond 2xx
rss_readytitle, description, author, category, image_url all present
enclosure_length_accurateStored audio_length within 5% of actual file size
pubdate_rfc2822All published_at values parseable as valid dates
guids_unique_and_stableNo duplicate GUIDs across episodes
Request
curl -X POST https://podclaw.io/api/shows/42/go-live \
  -H "Authorization: Bearer pc_live_your_key"
Success response (show has episodes)
{
  "success": true,
  "status": "live",
  "feed_url": "https://podclaw.io/api/shows/42/feed",
  "validation": {
    "passed": true,
    "checks": [
      { "check": "title_present", "passed": true },
      { "check": "category_valid", "passed": true },
      /* ... all 12 checks ... */
    ]
  },
  "trailer_generated": false,
  "distribution": {
    "spotify": {
      "status": "ready_to_submit",
      "submit_url": "https://podcasters.spotify.com/pod/submit/rss?feed=...",
      "instructions": "1. Open the Spotify submission URL ...\n4. Approval typically takes 1-5 business days"
    },
    "apple": {
      "status": "ready_to_submit",
      "submit_url": "https://podcastsconnect.apple.com/my-podcasts/new-feed?submitfeed=true&url=...",
      "instructions": "1. Sign in to Apple Podcasts Connect ...\n5. Once approved, your show appears globally"
    }
  }
}
Success response (show had 0 episodes — trailer auto-generated)
{
  "success": true,
  "status": "live",
  "feed_url": "https://podclaw.io/api/shows/42/feed",
  "trailer_generated": true,
  "trailer_episode": {
    "id": 101,
    "title": "My Podcast — Trailer",
    "audio_url": "https://podclaw.io/api/shows/42/trailer.mp3",
    "episode_type": "trailer"
  },
  "distribution": { /* spotify + apple submit URLs */ }
}
400 response (validation failed)
{
  "success": false,
  "error": "Pre-flight validation failed",
  "validation": {
    "passed": false,
    "checks": [ /* per-check results */ ],
    "failures": [
      "category_valid: \"Tech\" is not in Apple's official category list",
      "owner_email_present: Set owner_email on the show, or activate a billing plan"
    ]
  }
}
GET /api/shows/:id/trailer.mp3
Public

Streams the auto-generated TTS trailer audio for a show. Only exists if POST /go-live was called when the show had zero episodes. Podcast apps (Spotify, Apple, etc.) stream directly from this URL.

Headers returned
Content-Type: audio/mpeg
Content-Length: <bytes>
Cache-Control: public, max-age=86400

Analytics API

Programmatic access to download metrics, growth trends, and listener data.
Plan gating: Free → basic counts only  |  Builder → full analytics  |  Pro+ → full analytics with breakdowns

GET /api/shows/:id/analytics
Auth required

Show-level download metrics. Returns total downloads (all-time and last 30 days), episode count, average downloads per episode. Builder+ also gets top 10 episodes and a daily growth trend for the last 30 days.

Example response (Pro)
{
  "success": true,
  "show": { "id": 1, "title": "The AI Daily" },
  "analytics": {
    "plan_tier": "pro",
    "summary": {
      "total_episodes": 12,
      "downloads_all_time": 4823,
      "downloads_last_30d": 1240,
      "avg_downloads_per_episode": 401.9
    },
    "top_episodes": [
      { "id": 7, "title": "Ep 7: GPT-5", "published_at": "2025-11-01T00:00:00Z", "downloads": 980 }
    ],
    "growth_trend": {
      "period": "last_30d",
      "granularity": "daily",
      "data": [
        { "date": "2025-12-01", "downloads": 42 },
        { "date": "2025-12-02", "downloads": 58 }
      ]
    }
  }
}
GET /api/episodes/:id/analytics
Auth required

Episode-level metrics. Total downloads, daily breakdown for the last 30 days, and download velocity (downloads in first 24h / 7d / 30d after publish). Daily breakdown supports ?page=1&limit=30 pagination.

Example response (Pro)
{
  "success": true,
  "episode": {
    "id": 7, "title": "Ep 7: GPT-5", "published_at": "2025-11-01T00:00:00Z",
    "show": { "id": 1, "title": "The AI Daily" }
  },
  "analytics": {
    "plan_tier": "pro",
    "summary": { "total_downloads": 980 },
    "downloads_over_time": {
      "period": "last_30d", "granularity": "daily",
      "data": [{ "date": "2025-11-01", "downloads": 320 }],
      "pagination": { "page": 1, "limit": 30, "total": 10, "pages": 1 }
    },
    "download_velocity": { "first_24h": 320, "first_7d": 750, "first_30d": 980 }
  }
}
GET /api/analytics/downloads
Auth required

Download trends across all your shows. Returns a paginated time series and (Builder+) per-show and top-episode breakdowns.

Query parameters
period   day | week | month  (default: day)
from     ISO date string         (default: 30 days ago)
to       ISO date string         (default: now)
page     integer                 (default: 1)
limit    1–200                   (default: 30)
Example response (Pro)
{
  "success": true,
  "analytics": {
    "plan_tier": "pro",
    "period": "day",
    "from": "2025-11-16T00:00:00Z",
    "to": "2025-12-16T00:00:00Z",
    "total_downloads": 4823,
    "time_series": {
      "data": [
        { "period_start": "2025-11-16T00:00:00Z", "downloads": 55 }
      ],
      "pagination": { "page": 1, "limit": 30, "total": 30, "pages": 1 }
    },
    "by_show": [
      { "show_id": 1, "show_title": "The AI Daily", "downloads": 4823 }
    ],
    "top_episodes": [
      { "episode_id": 7, "episode_title": "Ep 7: GPT-5", "show_id": 1, "show_title": "The AI Daily", "downloads": 980 }
    ]
  }
}
GET /api/analytics/listeners
Auth required

Unique listener estimates and podcast-app breakdown. Listener count is estimated from IP-hash + user-agent deduplication. Geographic breakdown is not available.

Query parameters
from   ISO date string  (default: 30 days ago)
to     ISO date string  (default: now)
Example response (Pro)
{
  "success": true,
  "analytics": {
    "plan_tier": "pro",
    "from": "2025-11-16T00:00:00Z",
    "to": "2025-12-16T00:00:00Z",
    "summary": {
      "unique_listeners_estimate": 1842,
      "total_downloads": 4823,
      "note": "Unique listener estimate uses IP-hash + user-agent deduplication. Geographic data not available."
    },
    "podcast_apps": [
      { "app_name": "Apple Podcasts",  "downloads": 2100, "unique_listeners": 812, "pct_of_downloads": 43.5 },
      { "app_name": "Spotify",         "downloads": 1800, "unique_listeners": 690, "pct_of_downloads": 37.3 },
      { "app_name": "Overcast",        "downloads": 450,  "unique_listeners": 200, "pct_of_downloads": 9.3  },
      { "app_name": "Other",           "downloads": 473,  "unique_listeners": 140, "pct_of_downloads": 9.9  }
    ]
  }
}

Episode Scheduling & Status

Episodes can be in three states: draft, scheduled, or published. Scheduled episodes are flipped to published automatically when publish_at arrives (checked every 60 s). Draft and scheduled episodes are never included in the RSS feed.

POST /api/episodes/publish
🔑 Auth required

Publish immediately, save as draft, or schedule for future release.

New scheduling fields
"status":     "draft" | "scheduled" | "published"  // default: "published"
"publish_at": "2026-06-01T08:00:00Z"  // ISO datetime; auto-sets status="scheduled" if future
Schedule an episode
curl -X POST https://podclaw.io/api/episodes/publish \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "show_id": 42,
    "audio_url": "https://cdn.example.com/ep2.mp3",
    "title": "Episode 2 — Coming Soon",
    "publish_at": "2026-04-01T07:00:00Z"
  }'
Response
{
  "success": true,
  "episode": {
    "id": 88,
    "status": "scheduled",
    "publish_at": "2026-04-01T07:00:00.000Z",
    ...
  },
  "message": "Episode \"Episode 2 — Coming Soon\" scheduled for 2026-04-01T07:00:00.000Z."
}

Use GET /api/episodes?show_id=X&status=scheduled to list upcoming episodes, or status=draft for drafts.

Nested Episode Routes v1.3+

Episodes are now accessible under their parent show path. The flat /api/episodes routes remain for backward compatibility.

GET /api/shows/:id/episodes
🔑 Auth required · Scope: episodes:read

List episodes for a show. Optional ?status=draft|scheduled|published filter.

Request
curl https://podclaw.io/api/shows/42/episodes \
  -H "Authorization: Bearer YOUR_KEY"
GET /api/shows/:id/episodes/:ep_id
🔑 Auth required · Scope: episodes:read

Get a single episode by ID.

Request
curl https://podclaw.io/api/shows/42/episodes/88 \
  -H "Authorization: Bearer YOUR_KEY"
PATCH /api/shows/:id/episodes/:ep_id
🔑 Auth required · Scope: episodes:write

Partial update. GUID is immutable (RSS stability). Updatable fields: title, description, audio_url, audio_length, audio_type, duration_seconds, explicit, episode_type, season, episode_number, published_at, status, publish_at.

Promote a draft to published
curl -X PATCH https://podclaw.io/api/shows/42/episodes/88 \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"published"}'
DELETE /api/shows/:id/episodes/:ep_id
🔑 Auth required · Scope: episodes:write

Delete an episode. Removed from RSS immediately. Returns 204.

Request
curl -X DELETE https://podclaw.io/api/shows/42/episodes/88 \
  -H "Authorization: Bearer YOUR_KEY"

Scoped API Keys

Create API keys restricted to specific operations. Omit scopes (or pass null) for full access — all existing keys keep full access.

Available scopes
shows:read      List & get shows
shows:write     Create, update, delete shows
episodes:read   List & get episodes
episodes:write  Publish, update, delete episodes
webhooks:read   List webhooks
webhooks:write  Register & delete webhooks
POST /api/keys
🌐 Public

Create a scoped read-only key (example: safe to embed in a listener app).

Create read-only key
curl -X POST https://podclaw.io/api/keys \
  -H "Content-Type: application/json" \
  -d '{
    "name": "listener-app-readonly",
    "scopes": ["shows:read", "episodes:read"]
  }'
Response
{
  "success": true,
  "api_key": {
    "id": 7,
    "key": "pc_live_...",
    "scopes": ["shows:read", "episodes:read"],
    ...
  }
}

When a scoped key attempts an out-of-scope operation, it receives 403 Insufficient scope. Required: "shows:write"...

Webhooks

Register HTTPS endpoints to receive real-time event notifications. Every delivery is signed with HMAC-SHA256 — verify the X-PodClaw-Signature header to ensure authenticity.

Available events
show.created     show.updated     show.deleted
episode.created  episode.updated  episode.published  episode.deleted
*                // wildcard — receive all events
POST /api/v1/webhooks
🔑 Auth required · Scope: webhooks:write

Register a webhook. The secret is returned once — store it to verify future deliveries.

Register webhook
curl -X POST https://podclaw.io/api/v1/webhooks \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/podclaw",
    "events": ["episode.published", "episode.deleted"]
  }'
Response
{
  "success": true,
  "webhook": {
    "id": 1,
    "url": "https://yourapp.com/webhooks/podclaw",
    "events": ["episode.published", "episode.deleted"],
    "secret": "abc123...",    // shown ONCE — store securely
    "is_active": true
  },
  "note": "Store the secret securely..."
}
GET /api/v1/webhooks
🔑 Auth required · Scope: webhooks:read

List all registered webhooks. Secret is not returned.

Request
curl https://podclaw.io/api/v1/webhooks \
  -H "Authorization: Bearer YOUR_KEY"
DELETE /api/v1/webhooks/:id
🔑 Auth required · Scope: webhooks:write

Remove a webhook registration. Returns 204.

Request
curl -X DELETE https://podclaw.io/api/v1/webhooks/1 \
  -H "Authorization: Bearer YOUR_KEY"

Verifying Webhook Signatures

Every delivery includes X-PodClaw-Signature: sha256=<hex>. Compute HMAC-SHA256(secret, raw_request_body) and compare.

Verify in Node.js
const crypto = require('crypto');

function verify(secret, rawBody, signatureHeader) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}
Example delivery payload (episode.published)
{
  "event": "episode.published",
  "delivery_id": "d1e2f3...",
  "timestamp": "2026-04-01T07:00:01.234Z",
  "data": {
    "id": 88,
    "show_id": 42,
    "guid": "550e8400-...",
    "title": "Episode 2 — Coming Soon",
    "status": "published",
    "publish_at": "2026-04-01T07:00:00.000Z"
  }
}

Podcasting 2.0 RSS

All feeds now include Podcasting 2.0 namespace tags for improved directory compatibility.

New channel-level tags in GET /api/shows/:id/feed
<!-- Stable globally unique podcast identifier -->
<podcast:guid>550e8400-e29b-41d4-a716-446655440000</podcast:guid>

<!-- Content medium type -->
<podcast:medium>podcast</podcast:medium>

<!-- Verification / ownership text block -->
<podcast:txt purpose="verify">hosted-by-podclaw</podcast:txt>

The podcast:guid is derived deterministically from the show ID — it is stable across all feed regenerations. No action needed; all existing feeds gain these tags automatically. Feeds also now only include published episodes — draft and scheduled episodes remain hidden until their publish time.

File Upload v1.4+

Upload audio files directly to Cloudflare R2 — PodClaw stores them and returns a permanent public URL. You can then use that URL as audio_url when creating an episode, or combine upload + episode creation in a single multipart request. Storage: ~$0.015 / GB / mo, zero egress cost.

POST /api/v1/upload Auth required

Upload an audio file to Cloudflare R2. Returns a permanent public URL. Send as multipart/form-data with the file in a field named audio.

Accepted formats: mp3, m4a, wav, ogg, aac  |  Max size: 200 MB

Path A — Pre-upload, then create episode
# Step 1: Upload the audio file → get a URL
curl -X POST https://podclaw.io/api/v1/upload \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "audio=@episode.mp3"

# Response:
{
  "success": true,
  "url": "https://r2.podclaw.com/podclaw/1710000000_episode.mp3",
  "filename": "episode.mp3",
  "size": 45231890,
  "content_type": "audio/mpeg"
}

# Step 2: Create the episode using the returned URL
curl -X POST https://podclaw.io/api/shows/1/episodes \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Episode 1",
    "audio_url": "https://r2.podclaw.com/podclaw/1710000000_episode.mp3"
  }'
Path B — Upload + create episode in one multipart request
# Single request: file upload + episode creation
curl -X POST https://podclaw.io/api/shows/1/episodes \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "audio=@episode.mp3" \
  -F "title=Episode 1" \
  -F "description=Our first episode" \
  -F "episode_number=1"

# PodClaw uploads the file to R2 automatically, then creates the episode.
# Response is the same as a normal episode creation:
{
  "success": true,
  "message": "Episode created with uploaded audio file.",
  "episode": { "id": 42, "audio_url": "https://r2.podclaw.com/...", ... },
  "feed_url": "https://podclaw.io/api/shows/1/feed"
}

OP3 Analytics v1.4+

OP3 (Open Podcast Prefix Project) is a free, open-source service that counts podcast downloads. PodClaw automatically prefixes all RSS enclosure URLs with https://op3.dev/e/, so every download is tracked with zero configuration. No account, no API key.

OP3 prefix in RSS <enclosure>
<!-- PodClaw automatically generates this in your RSS feed -->
<enclosure
  url="https://op3.dev/e/https://podclaw.io/api/track/42"
  type="audio/mpeg"
  length="45231890"/>

<!-- When a listener downloads this episode, OP3 counts it,
     then transparently redirects to your actual audio file. -->
GET /api/v1/shows/:id/analytics Auth required

Proxies OP3 download analytics for a show. Returns per-episode download counts and totals. OP3 data becomes available within 24 hours of the first download through an OP3-prefixed feed.

Param Default Description
days30Lookback window (1–90 days)
limit100Max episodes returned (1–1000)
curl example
# Get OP3 download analytics for show 1 (last 30 days)
curl https://podclaw.io/api/v1/shows/1/analytics \
  -H "Authorization: Bearer YOUR_API_KEY"

# Custom date range
curl "https://podclaw.io/api/v1/shows/1/analytics?days=7&limit=50" \
  -H "Authorization: Bearer YOUR_API_KEY"

# Response:
{
  "success": true,
  "show": { "id": 1, "title": "My Podcast", "slug": "my-podcast" },
  "op3": {
    "podcast_guid": "550e8400-e29b-41d4-a716-446655440000",
    "op3_url": "https://op3.dev/show/550e8400-e29b-41d4-a716-446655440000",
    "period_days": 30,
    "total_downloads": 1842,
    "episodes": [
      { "episode_guid": "abc123", "title": "Ep 5", "downloads": 823 },
      { "episode_guid": "def456", "title": "Ep 4", "downloads": 612 }
    ]
  }
}

Cost: OP3 is free and open-source. PodClaw proxies the OP3 API on your behalf — no signup required. View your show directly on OP3: https://op3.dev/show/<podcast:guid>

Podcast Metadata Reference

A comprehensive guide to every metadata field, what it controls in RSS, and why it matters for podcast directories. All fields are optional — you only need what you use.

Apple Podcasts Categories

Categories must exactly match Apple's official list. PodClaw validates against this list and returns a 400 error with the full valid list if an invalid category is submitted.

Arts Business Comedy Education Fiction Government Health & Fitness History Kids & Family Leisure Music News Religion & Spirituality Science Society & Culture Sports TV & Film Technology True Crime

Show-Level Fields

FieldRSS TagDefaultWhy it matters
website_url<link>PodClaw show pageApple Podcasts requires your own domain. Sets the clickable link in podcast apps. Critical for Apple submission.
subtitle<itunes:subtitle>null (omitted)Short tagline shown in some podcast apps under the title
copyright<copyright>null (omitted)Copyright notice displayed in podcast apps
category<itunes:category text="…">"Society & Culture"Required by Apple Podcasts. Controls which directory section your show appears in. Must be an exact Apple category string.
subcategorynested <itunes:category>null (omitted)Narrows placement within a category, e.g. "Entrepreneurship" inside "Business"
owner_name<itunes:owner><itunes:name>author valueUsed by Apple for ownership verification. Falls back to author if not set.
owner_email<itunes:owner><itunes:email>account emailApple sends submission verification emails here. Required by Apple Podcasts.
explicit<itunes:explicit>falseRequired by Apple. Marks show as containing explicit content. Per-episode explicit overrides this.
itunes_type<itunes:type>"episodic"episodic: newest episode first (most shows). serial: oldest first — Apple numbers episodes sequentially and shows them in order. Use "serial" for narrative/story podcasts.
complete<itunes:complete>yesfalseTells directories the show is finished and no new episodes are coming. Removes it from "active shows" filters.
block<itunes:block>yesfalseAsks directories to remove this show from search. Use for takedowns or private feeds.
new_feed_url<itunes:new-feed-url>null (omitted)When migrating to a new host, set this to your new RSS URL. Directories will update their records and follow subscribers to the new feed. Set this before leaving PodClaw.
podcast_guid<podcast:guid>auto-generated UUIDPodcasting 2.0 globally unique identifier. Auto-generated on show creation and never changes. Used by OP3 analytics and modern podcast apps for cross-platform identity.
podcast_locked<podcast:locked>truePrevents other hosts from importing your feed without permission. Set to false only if you're intentionally transferring to another platform.
funding_url<podcast:funding url="…">null (omitted)Podcasting 2.0 feature. Links to your Patreon, Ko-fi, or support page. Displayed by Podcasting 2.0 apps like Fountain and Castamatic.
funding_text<podcast:funding> content"Support this podcast"Display text for the funding link button

Episode-Level Fields

FieldRSS TagDefaultWhy it matters
episode_number<itunes:episode>nullRequired by Apple for serial shows. Used for episode numbering in apps.
season<itunes:season>nullSeason number. Used by Apple to group episodes and show "Season X" labels in apps.
episode_type<itunes:episodeType>"full"full: regular episode. trailer: show preview (shown before first episode). bonus: supplemental content.
explicit<itunes:explicit>inherits showPer-episode explicit flag. If set, overrides the show-level explicit setting for this episode only.
episode_url<link> (per item)null (omitted)Link to the episode's own webpage (show notes page, blog post, etc.). Shown in some podcast apps.
episode_image_url<itunes:image href="…"/> (per item)show cover artPer-episode artwork. Overrides the show cover for this episode only. Great for series with unique artwork per episode.
transcript_url<podcast:transcript url="…" type="…"/>null (omitted)Podcasting 2.0 feature. Links to a transcript file. Supported formats: .txt (text/plain), .srt, .vtt (text/vtt), .json, .html. Apps like Overcast and Pocket Casts surface transcripts natively.
pub_date<pubDate>uses published_atIf set, overrides published_at as the RSS pubDate. Use for backdating historical episodes or publishing "as of" a specific date.

Complete RSS Feed Example

When all metadata fields are set, your feed looks like this:

RSS output — all fields set
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
  xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
  xmlns:podcast="https://podcastindex.org/namespace/1.0"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Knight Shift</title>
    <link>https://knightshiftpodcast.com</link>          <!-- website_url -->
    <copyright>© 2026 Josh Hodge</copyright>            <!-- copyright -->
    <itunes:author>Josh Hodge</itunes:author>
    <itunes:subtitle>Chess, culture, and community</itunes:subtitle>    <!-- subtitle -->
    <itunes:explicit>false</itunes:explicit>
    <itunes:type>episodic</itunes:type>                  <!-- itunes_type -->
    <itunes:category text="Sports">
      <itunes:category text="Chess"/>                   <!-- subcategory -->
    </itunes:category>
    <itunes:owner>
      <itunes:name>Josh Hodge</itunes:name>             <!-- owner_name -->
      <itunes:email>josh@knightshiftpodcast.com</itunes:email>
    </itunes:owner>
    <podcast:guid>a1b2c3d4-e5f6-7890-abcd-ef1234567890</podcast:guid>    <!-- auto-generated -->
    <podcast:locked owner="josh@knightshiftpodcast.com">yes</podcast:locked>
    <podcast:funding url="https://knightshiftpodcast.com/support">Support Knight Shift</podcast:funding>

    <item>
      <title>Episode 42: The King's Gambit</title>
      <itunes:season>3</itunes:season>
      <itunes:episode>42</itunes:episode>
      <itunes:episodeType>full</itunes:episodeType>
      <link>https://knightshiftpodcast.com/episodes/42</link>    <!-- episode_url -->
      <itunes:image href="https://cdn.example.com/ep42-art.jpg"/>    <!-- episode_image_url -->
      <podcast:transcript url="https://cdn.example.com/ep42.vtt" type="text/vtt"/>    <!-- transcript_url -->
    </item>
  </channel>
</rss>