Episodes REST API
Endpoints for creating, reading, updating, and deleting episodes, including each episode’s tier availability, and for 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-03 09: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-03 09:00:00",
"basic": "2026-11-10 09:00:00",
"free": "2026-11-17 09:00:00"
},
"uses_show_defaults": false,
"download_count": 342,
"is_imported": false,
"created_at": "2026-11-01 14:00:00",
"updated_at": "2026-11-03 09:00:00"
}
],
"total": 83,
"pages": 5,
"per_page": 20,
"page": 1
}
post_date, created_at and updated_at are the WordPress post’s own dates, so they are in the site’s time zone, as WordPress stores them.
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": "inherit",
"author": null,
"audio_url": "https://buzzsprout.com/ep84.mp3",
"video_url": null,
"preview_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-03 09:00:00",
"basic": "2026-11-10 09:00:00",
"free": "2026-11-17 09:00:00"
},
"use_show_defaults": false,
"visibility_mode": "inherit",
"tier_visibility": null,
"category_ids": [1, 3],
"references": [],
"guests": [],
"external_links": [],
"post_status": "draft"
}
Response: 201 Created with the full episode object.
Custom field values are not part of the episode API. Read and write them through their field groups with benecaster_get_episode_custom_fields(), benecaster_get_field() and benecaster_update_field(). A request that includes a custom_fields key is accepted and the key is ignored, like any other unknown key.
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 |
availability |
Tier slug → the date and time the episode reaches that tier. See Availability below |
use_show_defaults |
Boolean. true works the tier dates out from the show’s default availability. See Following the show’s defaults below |
explicit |
Three-value string enum: inherit, yes, or no. See below |
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 |
preview_url |
Short clip played to visitors who can’t hear the episode. Sanitised with esc_url_raw. Sending "" clears it; omitting the key or sending null leaves it unchanged, like video_url and the other URL fields. GET returns "" when unset. Web player only — never added to any feed. |
The explicit field
explicit is a three-value string enum — "inherit", "yes", or "no" — on both the request and the response.
"inherit"(the default) uses the show’s Explicit setting."yes"marks this episode explicit regardless of the show."no"marks this episode clean regardless of the show.
Unrecognised values coerce silently to "inherit" rather than erroring, so an unfamiliar future value falls back to the show default instead of leaking into the feed.
The show-level value uses a different enum — clean / yes / explicit — because Apple’s wording distinguishes the last two. The episode side does not mirror it, since Apple reads both as explicit and an episode override only ever needs to say explicit or clean.
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), source_link (read-only string; the imported item’s RSS <link>, its page on the old host, '' when there is none — not in the list response and not accepted on create/update), is_imported.
references holds each reference exactly as stored — {id, reference_id, label, library_label, url, description, display_label, group_id, display_order}. label is the resolved display label (this episode’s override when set); library_label is the library item’s own label. The array is not passed through the benecaster_episode_references filter, so an add-on that rewrites URLs on the episode page does not change what this returns.
The explicit field is the episode’s own override, not the resolved value. To know what the feed will actually emit, resolve it against the show’s setting — server-side that is EpisodeMeta::get_effective_explicit(); over REST, read the show’s Explicit value and apply inherit yourself.
PUT /benecaster/v1/episodes/{id}
Update an episode. All fields optional — only provided fields are updated.
Field notes:
| Field | Notes |
|---|---|
availability |
Replaces the episode’s tier dates. See Availability below |
use_show_defaults |
Boolean. See Following the show’s defaults below |
references |
Replaces the episode’s references. An entry carrying a reference_id updates that library item — its url, description and, when you send library_label, its label — so the change reaches every episode citing it. The library label is taken from library_label, never from label (which may be a display override). Omitted fields are left alone, not blanked. display_label, group_id and display_order stay per episode. An entry without reference_id finds or creates a library item by label and URL. |
podcast2_transcript_url |
Podcasting 2.0 transcript URL. Emitted as <podcast:transcript url="..." type="..." />; omitted from the feed when empty. |
podcast2_transcript_type |
MIME type for the transcript. One of text/plain, text/html, application/srt, text/vtt, application/json — any other value is dropped on save. |
podcast2_transcript_url_source |
Optional, write-only — not stored as its own field. Declares how podcast2_transcript_url was produced: media_picker or autodetect. Needed because the server can’t otherwise tell a picked or auto-detected URL from a typed one — the same string is reachable either way. Absent, manual, or an unrecognised value means a manual write, and benecaster_transcript_url_generated does not fire for it. |
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
Availability has no endpoint of its own. It is the availability field on the episode, read from any episode response and written with POST /episodes or PUT /episodes/{id}.
"availability": {
"premium": "2026-11-03 09:00:00",
"basic": "2026-11-10 09:00:00"
}
The times are in the site’s time zone, not UTC. That holds in both directions, because they are the values a podcaster sees and types in the episode editor. Benecaster converts them to UTC when it saves them. Send site-local times: a time already converted to UTC is converted again, and the episode unlocks early or late by the site’s offset. See Dates, Times and Time Zones.
Reading. The map holds one key per tier that has a date. A tier with no date is left out of the map, not returned as null. Buy-ups are left out too. They are set separately, with the required_buyups field, an array of buy-up IDs.
Writing. The map you send is the episode’s whole set of tier dates. Each tier with a date is created or updated. A tier of the show — from its tier mapping, or its built-in membership tiers — that you leave out, or send as null or "", loses its date and leaves that tier’s feed; {} clears every tier. A row that is not one of the show’s tiers, such as the one a required buy-up keeps, is never removed this way. Leave availability out of the request altogether and the dates are not changed. Writing fires the benecaster_availability_datetimes filter before the rows are saved and benecaster_availability_set after.
Following the show’s defaults
Every episode response carries uses_show_defaults — true while the episode’s tier dates follow the show’s default availability.
POST /episodes and PUT /episodes/{id} accept use_show_defaults:
trueworks the tier dates out from the show’s default availability against the episode’s current publish date, and the episode follows the defaults from then on: its dates are worked out again on every save. Anyavailabilitymap in the same request is ignored. On a show with no default availability,trueapplies nothing, and the episode does not follow.falsestops following. So does sending explicit dates, and onPUT, so does sending anyavailabilitymap.- Absent: on
POST, a request with neitheruse_show_defaultsnor any dates follows the defaults. OnPUT, a request with neither changes nothing.
The defaults themselves are the show’s default_availability and default_release_time fields, on GET and PUT /benecaster/v1/shows/{id}:
"default_availability": {
"tiers": {
"premium": { "offset_days": 0 },
"basic": { "offset_days": 7 }
}
},
"default_release_time": "09:00"
offset_days is how many days after release the tier gets a new episode, from 0 to 365; a tier left out never gets one. GET always returns tiers as an object, {} when the show has no defaults. default_release_time is the time of day the unlocks use, as HH:MM; "" clears it, so unlocks use each episode’s own publish time, and any other value is ignored.
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 when the episode page renders them. Not applied to REST responses |
See Also
Need this built rather than just documented? See our services →