Podcast Lifecycle API
Manages podcast lifecycle transitions: pause, unpause, and deletion.
Source: src/api/routes/podcast-lifecycle.tsBase Path: /api/podcast-lifecycle
Endpoints
GET /api/podcast-lifecycle/:podcastId
Returns the current lifecycle state, pause eligibility, and deletion status.
Auth: requireAuth() + requirePodcastRole('owner')
Response (200):
{
"success": true,
"data": {
"status": "active",
"hosting_type": "podcasterplus",
"paused_at": null,
"pause_expires_at": null,
"last_pause_ended_at": "2025-06-15T12:00:00Z",
"can_pause": true,
"is_paid_plan": true,
"next_pause_available_at": null,
"deletion_requested_at": null,
"deletion_redirect_url": null,
"deletion_scheduled_at": null
}
}Key fields:
| Field | Type | Description |
|---|---|---|
can_pause | boolean | Whether the podcast can be paused right now |
is_paid_plan | boolean | Whether the owner is on a paid tier |
next_pause_available_at | string | null | ISO date when pause becomes available again (12-month cooldown) |
POST /api/podcast-lifecycle/:podcastId/pause
Pauses a podcast for up to 90 days. RSS continues serving.
Auth: requireAuth() + requirePodcastRole('owner', { skipLifecycleCheck: true })
Validation:
| Check | Error |
|---|---|
Status must be active | 409: Cannot pause a podcast that is currently {status} |
| Paid plan required | 403: Pausing is only available on paid plans |
| 12-month cooldown | 409: You can only pause a podcast once every 12 months |
Response (200):
{
"success": true,
"data": {
"status": "paused",
"paused_at": "2026-04-15T10:00:00Z",
"pause_expires_at": "2026-07-14T10:00:00Z"
}
}Side effect: If this is the owner's last active podcast, pauses Stripe billing via pause_collection: { behavior: 'void' }.
POST /api/podcast-lifecycle/:podcastId/unpause
Reactivates a paused podcast immediately.
Auth: requireAuth() + requirePodcastRole('owner', { skipLifecycleCheck: true })
Validation: Status must be paused (409 otherwise).
Response (200):
{
"success": true,
"data": {
"status": "active"
}
}Side effect: Resumes Stripe billing (clears pause_collection).
POST /api/podcast-lifecycle/:podcastId/delete
Initiates podcast deletion with optional RSS redirect.
Auth: requireAuth() + requirePodcastRole('owner', { skipLifecycleCheck: true }) + zValidator('json', deleteSchema)
Request Body:
{
"confirm_name": "My Podcast",
"redirect_url": "https://newhost.com/feed.xml",
"redirect_days": 90
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
confirm_name | string | Yes | — | Must match podcast title (case-insensitive) |
redirect_url | string | null | No | null | New RSS feed URL for subscriber migration |
redirect_days | number (0-90) | No | 90 | Days before hard delete. 0 = no redirect window |
Validation:
| Check | Error |
|---|---|
Already pending_deletion | 409: Podcast is already scheduled for deletion |
| Name mismatch | 400: Podcast name does not match |
Response — External Podcast (200):
Hard-deleted immediately. No RSS or R2 assets to manage.
{
"success": true,
"data": {
"status": "deleted",
"deletion_requested_at": "2026-04-15T10:00:00Z",
"deletion_scheduled_at": null,
"deletion_redirect_url": null,
"immediate": true
}
}Response — Self-Hosted Podcast (200):
Scheduled for deletion after redirect window.
{
"success": true,
"data": {
"status": "pending_deletion",
"deletion_requested_at": "2026-04-15T10:00:00Z",
"deletion_scheduled_at": "2026-07-14T10:00:00Z",
"deletion_redirect_url": "https://newhost.com/feed.xml"
}
}Side effect: If no active/paused podcasts remain for the owner, cancels subscription at period end.
Error Responses
All endpoints use the standard error format:
{ "error": "Description of what failed" }| Status | Scenario |
|---|---|
| 400 | Invalid body, name mismatch |
| 401 | No auth token |
| 403 | Not podcast owner, free plan (pause) |
| 404 | Podcast not found |
| 409 | Invalid state transition (e.g., pausing a paused podcast) |
| 500 | Database or server error |
Constants
| Constant | Value | Description |
|---|---|---|
PAUSE_MAX_DAYS | 90 | Maximum pause duration |
PAUSE_COOLDOWN_MONTHS | 12 | Months between pauses |
DELETION_MAX_REDIRECT_DAYS | 90 | Maximum redirect window |
DELETION_DEFAULT_REDIRECT_DAYS | 90 | Default redirect window |
Related Documentation
- Podcast Lifecycle Guide - End-to-end flow documentation
- Lifecycle Manager Worker - Cron cleanup worker
- Subscription Lifecycle - Billing sync details