API Reference

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

https://podclaw.polsia.app

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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/api/shows/1/feed"}
Your RSS feed is live
curl
curl https://podclaw.polsia.app/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.polsia.app/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.

ParamTypeDescription
titlestringrequired Show title
slugstringoptional URL-friendly identifier (auto-generated from title)
descriptionstringoptional Show description
authorstringoptional Author name
languagestringoptional ISO 639-1 code (default: "en")
categorystringoptional iTunes category
image_urlstringoptional Cover art URL (3000x3000 recommended)
website_urlstringoptional Show website URL
explicitbooleanoptional Explicit content flag
Request
curl -X POST https://podclaw.polsia.app/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",
    "category": "Technology"
  }'
POST /api/episodes/publish
Auth required

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

ParamTypeDescription
show_idnumberrequired ID of the show
audio_urlstringrequired URL to audio file (MP3, M4A, etc.)
titlestringrequired Episode title
descriptionstringoptional Episode description / show notes
audio_typestringoptional MIME type of audio (default: "audio/mpeg")
audio_lengthnumberoptional File size in bytes (used in RSS enclosure)
duration_secondsnumberoptional Audio duration in seconds
seasonnumberoptional Season number
episode_numbernumberoptional Episode number
episode_typestringoptional "full", "trailer", or "bonus"
explicitbooleanoptional Explicit content flag
published_atstringoptional ISO date (default: now)
Request
curl -X POST https://podclaw.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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. updated_at is refreshed automatically.

FieldTypeDescription
titlestringShow title
descriptionstringShow description
authorstringAuthor name
image_urlstringCover art URL
categorystringApple Podcasts category
languagestringLanguage code (e.g. en)
explicitbooleanExplicit content flag
website_urlstringShow website
Request
curl -X PATCH https://podclaw.polsia.app/api/shows/1 \
  -H "Authorization: Bearer pc_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"title": "Daily AI Briefing v2", "description": "Smarter AI news, every morning."}'
Response — 200 OK
{
  "success": true,
  "show": {
    "id": 1,
    "title": "Daily AI Briefing v2",
    "description": "Smarter AI news, every morning.",
    "updated_at": "2026-03-17T14:30: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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/api/episodes/42 \
  -H "Authorization: Bearer pc_live_your_key"
Response — 204 No Content
(empty body)

Billing & Plans

Plan Tiers (Usage-Based)

PlanBase/moPer EpisodePer 1K DownloadsShowsEpisodes/mo
Sandbox$0510
Agent Pro$49$0.05$0.01UnlimitedUnlimited
Agent Scale$199$0.03$0.005UnlimitedUnlimited
EnterpriseCustomCustomCustomUnlimitedUnlimited

Every API key starts on the Sandbox plan. Upgrade anytime via the checkout links in GET /api/billing/plans. Sandbox is free forever. Paid plans charge a base fee + per-episode + per-download usage.

GET /api/billing/plans
Public

Returns all available plans with pricing and Stripe checkout links.

Request
curl https://podclaw.polsia.app/api/billing/plans
Response
{
  "success": true,
  "plans": [
    { "id": "sandbox", "name": "Sandbox", "price_usd": 0, "per_episode_usd": 0, "per_download_per_1k_usd": 0, "episodes_per_month": 10, "max_shows": 5, "checkout_url": null },
    { "id": "agent_pro", "name": "Agent Pro", "price_usd": 49, "per_episode_usd": 0.05, "per_download_per_1k_usd": 0.01, "episodes_per_month": null, "max_shows": null, "checkout_url": "https://buy.stripe.com/..." },
    { "id": "agent_scale", "name": "Agent Scale", "price_usd": 199, "per_episode_usd": 0.03, "per_download_per_1k_usd": 0.005, "episodes_per_month": null, "max_shows": null, "checkout_url": "https://buy.stripe.com/..." },
    { "id": "enterprise", "name": "Enterprise", "price_usd": null, "contact": "hello@podclaw.io" }
  ]
}
GET /api/billing
Auth required

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

Request
curl https://podclaw.polsia.app/api/billing \
  -H "Authorization: Bearer pc_live_your_key"
Response
{
  "success": true,
  "billing": {
    "plan": "agent_pro",
    "plan_name": "Agent Pro",
    "price_usd": 49,
    "per_episode_usd": 0.05,
    "per_download_per_1k_usd": 0.01,
    "billing_cycle": {
      "start": "2026-03-01T00:00:00Z",
      "end": "2026-03-31T00:00:00Z",
      "days_remaining": 18
    },
    "usage": {
      "episodes": {
        "used": 23,
        "limit": null, // null = no hard cap (usage-based)
        "remaining": null,
        "estimated_usage_cost_usd": 1.15 // 23 * $0.05
      },
      "downloads": {
        "total": 4820, // tracked plays via /api/track/:id in RSS enclosures
        "estimated_usage_cost_usd": 0.0482 // 4.82K * $0.01
      },
      "storage": { "used_gb": 2.4 }
    },
    "upgrade_options": [ /* agent_scale, enterprise */ ]
  }
}
POST /api/billing/activate
Auth required

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

ParamTypeDescription
planstringrequired "agent_pro" or "agent_scale"
emailstringoptional Email used for Stripe checkout
Request
curl -X POST https://podclaw.polsia.app/api/billing/activate \
  -H "Authorization: Bearer pc_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"plan": "agent_pro", "email": "you@example.com"}'
Response
{
  "success": true,
  "message": "Plan upgraded to Agent Pro! Base fee: $49/mo + $0.05/episode.",
  "billing": {
    "plan": "agent_pro",
    "plan_name": "Agent Pro",
    "price_usd": 49,
    "per_episode_usd": 0.05,
    "per_download_per_1k_usd": 0.01,
    "activated_at": "2026-03-14T00:00:00.000Z",
    "billing_cycle_start": "2026-03-14T00:00:00.000Z"
  }
}

Quota Enforcement

Sandbox is limited to 10 episodes/month. When you exceed the limit, POST /api/episodes/publish returns 429 Too Many Requests. Paid plans (Agent Pro, Agent Scale) have no hard episode cap — you pay per episode published.

429 Response (Sandbox quota exceeded)
{
  "success": false,
  "error": "Episode limit reached. Your Sandbox plan allows 10 episodes/month (10 used). Upgrade at GET /api/billing",
  "usage": {
    "episodes_used": 10,
    "episodes_limit": 10,
    "plan": "sandbox"
  }
}

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.polsia.app/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.polsia.app/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.polsia.app/api/shows/42/feed",
  "trailer_generated": true,
  "trailer_episode": {
    "id": 101,
    "title": "My Podcast — Trailer",
    "audio_url": "https://podclaw.polsia.app/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: Sandbox → basic counts only  |  Agent Pro → full analytics (time series, breakdowns)  |  Agent Scale → everything

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. Agent Pro+ also gets top 10 episodes and a daily growth trend for the last 30 days.

Example response (Agent Pro)
{
  "success": true,
  "show": { "id": 1, "title": "The AI Daily" },
  "analytics": {
    "plan_tier": "agent_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 (Agent 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": "agent_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 (Agent Pro+) 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 (Agent Pro)
{
  "success": true,
  "analytics": {
    "plan_tier": "agent_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 (Agent Pro)
{
  "success": true,
  "analytics": {
    "plan_tier": "agent_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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/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.polsia.app/api/v1/shows/1/analytics \
  -H "Authorization: Bearer YOUR_API_KEY"

# Custom date range
curl "https://podclaw.polsia.app/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>