Skip to content

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):

json
{
	"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:

FieldTypeDescription
can_pausebooleanWhether the podcast can be paused right now
is_paid_planbooleanWhether the owner is on a paid tier
next_pause_available_atstring | nullISO 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:

CheckError
Status must be active409: Cannot pause a podcast that is currently {status}
Paid plan required403: Pausing is only available on paid plans
12-month cooldown409: You can only pause a podcast once every 12 months

Response (200):

json
{
	"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):

json
{
	"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:

json
{
	"confirm_name": "My Podcast",
	"redirect_url": "https://newhost.com/feed.xml",
	"redirect_days": 90
}
FieldTypeRequiredDefaultDescription
confirm_namestringYesMust match podcast title (case-insensitive)
redirect_urlstring | nullNonullNew RSS feed URL for subscriber migration
redirect_daysnumber (0-90)No90Days before hard delete. 0 = no redirect window

Validation:

CheckError
Already pending_deletion409: Podcast is already scheduled for deletion
Name mismatch400: Podcast name does not match

Response — External Podcast (200):

Hard-deleted immediately. No RSS or R2 assets to manage.

json
{
	"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.

json
{
	"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:

json
{ "error": "Description of what failed" }
StatusScenario
400Invalid body, name mismatch
401No auth token
403Not podcast owner, free plan (pause)
404Podcast not found
409Invalid state transition (e.g., pausing a paused podcast)
500Database or server error

Constants

ConstantValueDescription
PAUSE_MAX_DAYS90Maximum pause duration
PAUSE_COOLDOWN_MONTHS12Months between pauses
DELETION_MAX_REDIRECT_DAYS90Maximum redirect window
DELETION_DEFAULT_REDIRECT_DAYS90Default redirect window

Internal documentation - Not for public distribution