Skip to main content

Subscribers REST API

Admin-facing REST endpoints for reading and managing a show’s subscribers.

Base path: benecaster/v1

Auth: manage_options + X-WP-Nonce on every endpoint below.


GET /benecaster/v1/shows/{id}/subscribers

Paginated, filtered subscriber list with summary counts.

Query parameters:

Parameter Default Notes
page 1
per_page 20 Capped at 100
status active
tier — Tier slug
search — Matches email or display name
last_seen — Engagement filter
migration_status —
token_type — Any registered type: subscriber, follower, purchaser, or an add-on-registered type, matched on its exact slug (subscriber excludes purchasers). Narrows items only — the response summary always covers the whole show regardless. An unregistered value, including a deactivated add-on’s type, returns 400 rest_invalid_param.
orderby created_at
order desc

Response envelope:

{
  "items": [ ... ],
  "total": 226,
  "pages": 12,
  "per_page": 20,
  "page": 1,
  "summary": { "active": 226, "paying": 84, "followers": 142, "by_tier": {}, "by_type": { "subscriber": 84, "follower": 142, "purchaser": 6 } },
  "shadow_summary": { },
  "token_types": [
    { "slug": "subscriber", "label": "Subscriber" },
    { "slug": "follower", "label": "Follower" },
    { "slug": "purchaser", "label": "Purchaser" }
  ]
}

summary.by_type — active listeners per stored token_type, including any orphaned type (its registering add-on has since been deactivated).

token_types — every type registered for this show, built-ins first (subscriber, follower, purchaser), then add-on-registered types by slug. Each entry is { slug, label }.

Each item:

{
  "id": 47,
  "user_id": 312,
  "email": "listener@example.com",
  "display_name": "Sam Reyes",
  "avatar_url": "https://…",
  "tier_slug": "supporter",
  "tier_name": "Supporter",
  "badges": [],
  "token_prefix": "bc_9f2a",
  "token_type": "subscriber",
  "status": "active",
  "created_at": "2026-03-04 11:20:00",
  "last_accessed_at": "2026-08-30 07:41:00",
  "last_accessed_human": "yesterday",
  "engagement_level": "active",
  "is_migration": false,
  "is_over_limit": false,
  "over_limit_effect": null,
  "is_manual_grant": false,
  "manual_expiry_at": null,
  "token_type_label": "Subscriber",
  "token_type_base": "subscriber"
}

over_limit_effect

"demoted" | "locked" | null.

What the over-limit flag actually does to this listener:

Value Meaning
"demoted" The token still resolves and serves a feed — the public one. Every flagged paying token, and every flagged token under auto_upgrade.
"locked" The feed URL returns 410 and the listener receives nothing. Only a free-tier token, only under cap enforcement.
null Not flagged. The ordinary case.

This is the outcome, not its inputs. Do not reconstruct it. is_over_limit is unchanged and still present beside it, but a flagged token is not enough to tell you what happened — a demoted listener and a locked one are both is_over_limit: true and their situations are opposites. The enforcement preference is deliberately not published, so the outcome cannot be derived client-side even with the tier in hand. Read this field.

That is the whole reason it exists. Labelling every flagged token “Receiving public feed” would be wrong for a locked listener, the one person that is not happening to, and a podcaster investigating a dead feed would read that the feed was being served and stop looking. The feed and this payload read the same rule, so they cannot disagree.

token_type — "subscriber", "follower", "purchaser", or an add-on-registered type. See Follower Tier REST.

token_type_label — human-readable label for this row’s token_type. An orphaned type (its add-on since deactivated) takes its base type’s label.

token_type_base — the resolved base of token_type: subscriber or follower.


Other subscriber endpoints

Method Route Purpose
GET /subscribers/{id} Full subscriber detail
GET /subscribers/duplicates Groups of subscribers sharing an email address
POST /subscribers/{id}/reset-token Generate a new token, invalidate the old one
POST /subscribers/{id}/email-feed-link Email the subscriber the feed URL they already have; resets nothing; fires benecaster_feed_link_requested
PUT /subscribers/{id}/tier Change tier; fires benecaster_subscriber_tier_overridden
DELETE /subscribers/{id} Revoke access; fires benecaster_subscriber_access_revoked
POST /subscribers/bulk-reset-tokens Reset tokens for multiple subscribers
POST /subscribers/bulk-revoke Revoke access for multiple subscribers
POST /subscribers/bulk-tier-change Change tier for multiple subscribers
POST /tools/export/subscribers Write a subscriber CSV for one show and return its download link

POST /subscribers/{id}/reset-token on a revoked subscriber reinstates them, whatever the revocation reason, and sends the token.reactivated webhook. Nothing else brings back someone a podcaster removed.

POST /subscribers/{id}/reset-token response. feed_url is the full, working URL of the new token, built by benecaster_get_feed_url() (so a benecaster_token_url filter applies) — this is the only admin-facing response that ever carries a working URL; GET /subscribers/{id}‘s own feed_url stays masked. The response carries Cache-Control: no-store, and feed_url should never be logged. email_sent is true only when the request’s send_email was true and the subscriber’s show has its token-reset email switched on (Show Settings → Subscription → Welcome and Feed URL Emails) — with the switch off it is false even when send_email was requested, though the reset still happens and feed_url is still returned. true means the email was queued, not delivered.

POST /subscribers/{id}/email-feed-link. Emails the subscriber their existing, working feed URL as a Feed Link email (feed_link). Nothing is reset, so their podcast app keeps working; use this rather than a reset when a subscriber has lost their URL. The body takes an optional custom_message, added to the email. The response is { email_sent: true, sent_to, sent_at }, where sent_to is a partly masked address (d•••@example.com) and sent_at is UTC. It never includes the URL — no admin surface shows a subscriber’s working feed URL — and it carries Cache-Control: no-store. It sends whatever the show’s token-reset email switch says, because the podcaster asked for it explicitly. Two 409 answers are ordinary outcomes, not faults:

  • benecaster_feed_url_unrecoverable — Benecaster cannot recover this subscriber’s URL (a token created before encrypted copies were stored, or a changed encryption key), so there is nothing to send. Nothing is sent; a reset is the remaining option.
  • benecaster_no_email_address — the subscriber’s account has no email address.

POST /subscribers/bulk-reset-tokens response: { processed: int, emails_sent: int, errors: int[] }. emails_sent counts how many were requested and queued, not delivered, subject to the same per-show token-reset-email switch as the single-subscriber endpoint, checked per subscriber’s own show since a bulk selection can span shows.

POST /tools/export/subscribers. Writes a CSV of one show’s listeners to the exports directory, the same file Settings → Tools → Subscriber CSV and the Subscribers screen’s Export button produce. Requires manage_options. The body takes:

Field
show_id Required.
tier_slug Optional. One tier’s listeners only.
status active (the default), revoked or all.
token_type Optional. One listener type, matched on its exact slug. A type nobody is registered under gives a file with the header row only, never every listener.
search Optional. Part of an email address.
last_seen Optional. recent, inactive, dormant or never. Any other value is refused with 400 rest_invalid_param, rather than exporting more than the screen showed.

token_type, search and last_seen narrow the file exactly as they narrow GET /shows/{id}/subscribers. The response is 201 with id, filename, file_size, created_at and download_url; download the file from download_url while signed in as an administrator. The columns are listed in Export File Format.

Bulk request body. All three bulk routes take a user_ids array of subscriber IDs, up to 200 per request. A subscriber ID is the id field from GET /shows/{id}/subscribers or GET /subscribers/{id}. Despite the parameter’s name, these are not WordPress user IDs. A WordPress user ID sent by mistake is not rejected: if it matches a subscriber ID, the route acts on that subscriber, who may be on a different show.

See Also

Need this built rather than just documented? See our services →