Skip to main content

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:

  • true works 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. Any availability map in the same request is ignored. On a show with no default availability, true applies nothing, and the episode does not follow.
  • false stops following. So does sending explicit dates, and on PUT, so does sending any availability map.
  • Absent: on POST, a request with neither use_show_defaults nor any dates follows the defaults. On PUT, 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 →