Episodes REST API
Endpoints for creating, reading, updating, and deleting episodes; managing episode availability; and managing per-show episode type definitions. All endpoints require manage_options and a valid X-WP-Nonce header unless noted.
Episodes
GET /benecaster/v1/shows/{id}/episodes
List episodes for a show. Supports filtering, sorting, and pagination.
Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
page |
int | 1 | Page number |
per_page |
int | 20 | Items per page (max 100) |
status |
string | any |
publish, draft, scheduled, any |
orderby |
string | date |
date, episode_number, title |
order |
string | DESC |
ASC or DESC |
search |
string | — | Search by title or description |
episode_type |
string | — | Filter by type slug (e.g. full, trailer, bonus) |
imported |
bool | — | true to return only feed-sync imports |
Response:
{
"items": [
{
"id": 201,
"show_id": 45,
"title": "Ep 84: Interview with Sarah Chen",
"episode_number": 84,
"season_number": null,
"episode_type": "full",
"status": "publish",
"post_date": "2026-11-03T09:00:00",
"duration": "47:23",
"file_size": 44368896,
"audio_url": "https://buzzsprout.com/ep84.mp3",
"artwork_url": "https://...",
"category_ids": [1, 3],
"availability": {
"premium": "2026-11-03T09:00:00",
"basic": "2026-11-10T09:00:00",
"free": "2026-11-17T09:00:00",
"public": null
},
"download_count": 342,
"is_imported": false,
"created_at": "2026-11-01T14:00:00",
"updated_at": "2026-11-03T09:00:00"
}
],
"total": 83,
"pages": 5,
"per_page": 20,
"page": 1
}
POST /benecaster/v1/episodes
Create a new episode.
Body:
{
"show_id": 45,
"title": "Ep 84: Interview with Sarah Chen",
"description_rss": "This week I chat with Sarah Chen about...",
"content": "<p>Full show notes here...</p>",
"episode_number": 84,
"season_number": null,
"episode_type": "full",
"explicit": null,
"author": null,
"audio_url": "https://buzzsprout.com/ep84.mp3",
"video_url": null,
"embed_audio": null,
"embed_video": null,
"media_type": "audio",
"duration": "47:23",
"file_size": 44368896,
"mime_type": "audio/mpeg",
"artwork_url": null,
"web_visible_datetime": "2026-11-03T09:00:00",
"availability": {
"premium": "2026-11-03T09:00:00",
"basic": "2026-11-10T09:00:00",
"free": "2026-11-17T09:00:00",
"public": null
},
"visibility_mode": "inherit",
"tier_visibility": null,
"category_ids": [1, 3],
"references": [],
"guests": [],
"custom_fields": [],
"external_links": [],
"post_status": "draft"
}
Response: 201 Created with the full episode object.
Field notes:
| Field | Notes |
|---|---|
description_rss |
Plain text or basic HTML — placed in the RSS feed <description> |
content |
Full show notes; rendered on the episode page |
episode_type |
Must be a slug defined for this show via POST /shows/{id}/episode-types |
visibility_mode |
inherit (use show defaults), custom (use tier_visibility), or public |
tier_visibility |
Object of tier slugs to datetime strings; used when visibility_mode is custom |
category_ids |
Array of episode category IDs for this show |
post_status |
draft or publish |
GET /benecaster/v1/episodes/{id}
Get a single episode with all metadata.
Response: Full episode object — same shape as items in the list response plus computed fields: feed_urls, source_guid (for imported episodes), is_imported.
PUT /benecaster/v1/episodes/{id}
Update an episode. All fields optional — only provided fields are updated.
Response: Updated episode object.
DELETE /benecaster/v1/episodes/{id}
Delete an episode and all associated availability records.
Response: 200 OK with { "deleted": true }
POST /benecaster/v1/episodes/{id}/detect-metadata
Detect audio duration and file size from the episode’s audio URL. Makes a HEAD request to the URL and reads Content-Length and media duration. Premium only.
Response:
{
"duration": "47:23",
"duration_seconds": 2843,
"file_size": 44368896,
"mime_type": "audio/mpeg",
"detected_at": "2026-11-01T14:00:00"
}
Errors:
| Code | HTTP | Condition |
|---|---|---|
detection_failed |
400 | URL unreachable or doesn’t support HEAD requests |
Availability
GET /benecaster/v1/episodes/{id}/availability
Get availability datetimes for all configured tiers.
Response:
{
"episode_id": 201,
"inherits_show_defaults": true,
"availability": {
"premium": "2026-11-03T09:00:00",
"basic": "2026-11-10T09:00:00",
"free": "2026-11-17T09:00:00",
"public": null
},
"next_unlock": {
"tier": "basic",
"datetime": "2026-11-10T09:00:00",
"in_seconds": 604800
}
}
null for a tier means that tier never gets access (or has no windowing rule set). next_unlock is null when no future unlock is pending.
PUT /benecaster/v1/episodes/{id}/availability
Update availability datetimes for an episode.
Body:
{
"availability": {
"premium": "2026-11-03T09:00:00",
"basic": "2026-11-10T09:00:00",
"free": "2026-11-17T09:00:00",
"public": null
},
"inherit_show_defaults": false
}
Fires benecaster_availability_set. Invalidates the feed cache for affected show/tiers.
Member Thanks
GET /benecaster/v1/episodes/{id}/member-thanks
Returns a list of members to thank for the episode, using the configured query strategy.
Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
query_type |
string | random_cycling |
Query strategy. Built-in: new_since_last_episode, random_cycling, milestone. Additional types via benecaster_member_thanks_query_types |
count |
int | 5 | Number of members to return. 0 returns all results |
tier |
string | — | Filter results to a specific tier slug |
Response:
{
"episode_id": 201,
"show_id": 45,
"query_type": "random_cycling",
"is_accepted_list": false,
"members": [
{
"user_id": 1042,
"display_name": "Sarah Chen",
"tier_slug": "premium",
"joined_at": "2026-09-14T11:32:00"
}
]
}
is_accepted_list is true when the Podcast Workflow add-on is active and a list has been accepted for this episode via the Episode Workbench — in that case the accepted list is returned regardless of query_type and count.
Errors:
| Code | HTTP | Condition |
|---|---|---|
rest_not_found |
404 | Episode does not exist |
invalid_query_type |
400 | query_type is not a registered strategy |
Episode Types
Episode types are per-show labels that map to iTunes episode types (full, trailer, bonus). The default types (Full Episode, Trailer, Bonus, Sponsored) are created automatically for every new show.
GET /benecaster/v1/shows/{id}/episode-types
Returns all episode type definitions for a show.
Response:
[
{ "slug": "full", "label": "Full Episode", "itunes_type": "full", "sort_order": 0, "is_default": true, "episode_count": 82 },
{ "slug": "trailer", "label": "Trailer", "itunes_type": "trailer", "sort_order": 1, "is_default": true, "episode_count": 1 },
{ "slug": "bonus", "label": "Bonus", "itunes_type": "bonus", "sort_order": 2, "is_default": true, "episode_count": 0 },
{ "slug": "sponsored", "label": "Sponsored", "itunes_type": "full", "sort_order": 3, "is_default": true, "episode_count": 0 }
]
episode_count is used by the episode editor delete/reassign modal to warn when deleting a type that has episodes.
POST /benecaster/v1/shows/{id}/episode-types
Creates a new episode type for a show.
Body:
{
"label": "Sponsored Bonus",
"itunes_type": "full"
}
slug is auto-derived server-side from label. Made unique within the show by appending -2, -3, etc. if needed.
Response: 201 Created with the type object.
Errors:
| Code | HTTP | Condition |
|---|---|---|
invalid_param |
400 | itunes_type not one of full, trailer, bonus |
invalid_param |
400 | label empty or exceeds 255 characters |
PUT /benecaster/v1/shows/{id}/episode-types/{slug}
Updates the label and/or iTunes mapping of an existing type. Slug is immutable — episodes store the slug in _benecaster_episode_type and renaming it would orphan existing episodes.
Body: label and/or itunes_type — all fields optional.
Response: Updated type object.
DELETE /benecaster/v1/shows/{id}/episode-types/{slug}
Deletes an episode type. All episodes using this type are bulk-reassigned to another type in the same transaction.
Required query param: reassign_to — slug of the type to reassign to.
Response:
{
"deleted": "sponsored",
"reassigned_count": 14
}
Errors:
| Code | HTTP | Condition |
|---|---|---|
missing_reassign_to |
400 | reassign_to query param absent |
invalid_reassign_to |
400 | reassign_to slug doesn’t exist on this show |
reassign_to_self |
400 | reassign_to equals the slug being deleted |
last_type |
409 | This is the last remaining type — shows must always have at least one |
rest_not_found |
404 | Slug doesn’t exist on this show |
Example:
DELETE /wp-json/benecaster/v1/shows/45/episode-types/sponsored?reassign_to=full
Feed Sync (Recurring)
POST /benecaster/v1/shows/{id}/sync
Trigger a manual feed sync — imports only new episodes not yet in WordPress (episodes whose GUID is not already stored). Premium only.
Body:
{
"source_feed_url": "https://feeds.buzzsprout.com/12345.rss"
}
Response:
{
"episodes_found": 86,
"episodes_new": 3,
"drafts_created": 3,
"draft_ids": [201, 202, 203],
"log_id": 47
}
For one-time bulk import of all episodes during setup, use POST /shows/{id}/import (see Setup Wizard REST API) instead.
Filters
| Filter | Where it fires |
|---|---|
benecaster_episode_auto_number |
Filters the auto-assigned episode number on first publish |
benecaster_episode_itunes_type |
Filters the resolved iTunes episode type during feed compilation |
benecaster_episode_is_accessible |
Fires inside benecaster_user_can_access_episode() — override episode access for any tier |
benecaster_episode_references |
Filters the references array in REST responses |