Skip to main content

Export File Format Reference

This page documents the machine-readable structure of Benecaster export files. It is intended for developers building migration tools, importers, or data pipelines that consume Benecaster exports programmatically.

For the admin UI that generates these files, see Exporting Your Data.


File Types

Benecaster produces two distinct export file types:

File When used Format
benecaster-export-{timestamp}.json.gz Full data export (Settings → Tools → Export) Gzip-compressed JSON
benecaster-subscribers-{show}-{tier}-{status}-{timestamp}.csv Subscriber CSV export (Settings → Tools → Export → Subscriber CSV) UTF-8 CSV with BOM

Both file types are written to wp-content/uploads/benecaster/exports/ on the WordPress server and are not directly web-accessible (blocked by .htaccess). They are downloaded exclusively via the REST API download endpoint:

GET /wp-json/benecaster/v1/tools/exports/{id}/download

where {id} is the filename stem (everything before .json.gz or .csv).


JSON Export Format

Decompressing

The full export is a gzip-compressed JSON file. Decompress with any standard gzip tool before parsing:

gunzip benecaster-export-2026-06-11-143000.json.gz

Python, Node.js, and most languages can decompress gzip streams natively.

Top-Level Envelope

Every JSON export has the same outer structure:

{
  "exported_at": "2026-06-11T14:30:00+00:00",
  "site_url": "https://example.com",
  "benecaster_version": "1.2.0",
  "show_id_filter": null,
  "datasets": {
    "subscribers": [...],
    "shows_episodes": [...],
    "tiers": [...],
    "migration_records": {...},
    "token_records": [...]
  }
}
Field Type Description
exported_at string (ISO 8601) UTC timestamp when the export was generated
site_url string The WordPress site URL from get_site_url()
benecaster_version string Plugin version at export time
show_id_filter integer or null When set, all subscriber/tier data is scoped to a single show; null means all shows
datasets object One key per dataset selected at export time — see below

Only datasets selected when triggering the export are present. A migration tool must handle any combination of the five keys being absent.


Dataset: subscribers

An array of subscriber records. Each record corresponds to one active RSS token and the WordPress user who holds it.

{
  "id": "42",
  "user_id": "391",
  "show_id": "10",
  "tier_slug": "premium",
  "status": "active",
  "join_date": "2026-01-15 10:30:00",
  "last_accessed_at": "2026-06-10 08:22:41",
  "email": "listener@example.com",
  "display_name": "Jane Doe",
  "tier_name": "Premium Tier"
}
Field Description
id Token record ID (string — see Type Note below)
user_id WordPress user ID (string)
show_id Show the token belongs to (string)
tier_slug Internal tier slug used in feed gating
status "active" or "revoked"
join_date UTC datetime when the token was created (Y-m-d H:i:s)
last_accessed_at UTC datetime of the most recent feed poll; null if the feed has never been polled
email Subscriber’s email address
display_name WordPress display name
tier_name Human-readable tier name from benecaster_tier_map; null if the tier is no longer mapped

Type Note: All fields in this dataset are JSON strings because the underlying query returns ARRAY_A from $wpdb->get_results(). Numeric IDs (id, user_id, show_id) are strings in the JSON and must be cast to integers by the consumer.


Dataset: shows_episodes

An array of shows, each embedding its episodes. This is the only dataset that uses native JSON types for numeric fields.

{
  "id": 10,
  "title": "My Private Podcast",
  "slug": "my-private-podcast",
  "status": "publish",
  "created_at": "2026-01-01 00:00:00",
  "modified_at": "2026-06-01 12:00:00",
  "episodes": [
    {
      "id": 105,
      "title": "Episode 3: Deep Dive",
      "slug": "episode-3-deep-dive",
      "status": "publish",
      "publish_date": "2026-06-01 00:00:00",
      "modified_date": "2026-06-01 00:00:00",
      "audio_url": "https://cdn.example.com/ep3.mp3",
      "duration": "3600",
      "file_size": 52428800,
      "episode_number": 3,
      "season_number": 1,
      "episode_type": "full",
      "explicit": false,
      "source_guid": "https://example.com/feed/ep3",
      "description": "In this episode..."
    }
  ]
}

Show fields:

Field Type Description
id integer WordPress post ID
title string Show title
slug string WordPress post slug (URL-safe)
status string WordPress post status ("publish", "draft", etc.)
created_at string UTC datetime of show creation
modified_at string UTC datetime of last modification
episodes array All published episodes for this show

Episode fields:

Field Type Description
id integer WordPress post ID
title string Episode title
slug string WordPress post slug
status string WordPress post status
publish_date string UTC datetime the episode was published
modified_date string UTC datetime of last modification
audio_url string Full URL to the audio file at the hosting provider
duration string Duration in seconds as a string (e.g. "3600"); empty string if not set
file_size integer File size in bytes; 0 if not set
episode_number integer Episode number; 0 if not set
season_number integer Season number; 0 if not set
episode_type string "full", "trailer", or "bonus"
explicit boolean Whether the episode is marked explicit
source_guid string The GUID from the original RSS feed when this episode was imported via feed sync; empty string for manually created episodes
description string RSS-optimized plain-text description (stored as _benecaster_description_rss)

Dataset: tiers

An array of tier-map rows. Each row describes the mapping between one Benecaster internal tier and one external membership level from the active subscription plugin.

{
  "show_id": "10",
  "internal_tier_slug": "premium",
  "internal_tier_name": "Premium",
  "internal_tier_order": "1",
  "external_tier_id": "membership-level-5",
  "external_tier_name": "Pro Member",
  "plugin_slug": "memberpress",
  "is_free_tier": "0",
  "is_public_tier": "0"
}
Field Description
show_id Show this tier belongs to (string)
internal_tier_slug Benecaster’s stable identifier for this tier; used in feed gating and in the subscribers dataset
internal_tier_name Human-readable tier name
internal_tier_order Display sort order (string; cast to integer)
external_tier_id The ID or slug this tier maps to in the subscription plugin
external_tier_name The name of the level in the subscription plugin at the time of last sync
plugin_slug Which subscription plugin this row belongs to (e.g. "memberpress", "pmpro", "rcp")
is_free_tier "1" if this tier requires no active paid membership; "0" otherwise
is_public_tier "1" if this tier’s episodes appear in the public RSS feed without a token; "0" otherwise

Type Note: All fields are JSON strings. is_free_tier and is_public_tier use "0" / "1" rather than boolean.


Dataset: migration_records

An object with two keys — imports and patrons — rather than a flat array. Both can be empty arrays when no migration data exists.

{
  "imports": [
    {
      "id": "1",
      "show_id": "10",
      "source_platform": "patreon",
      "import_date": "2026-01-15 09:00:00",
      "total_imported": "250",
      "total_completed": "198",
      "total_dropped": "12",
      "grace_period_days": "30",
      "grace_ends_at": "2026-02-15 09:00:00",
      "status": "complete",
      "imported_by": "1",
      "notes": null
    }
  ],
  "patrons": [
    {
      "id": "1",
      "import_id": "1",
      "email": "patron@example.com",
      "source_tier": "Gold Patron",
      "mapped_tier_slug": "premium",
      "wp_user_id": "391",
      "status": "subscribed",
      "invite_sent_at": "2026-01-15 10:00:00",
      "completed_at": "2026-01-16 11:00:00",
      "grace_period_ends_at": null
    }
  ]
}

Import fields:

Field Description
id Import batch ID
show_id Show this import belongs to
source_platform Platform name ("patreon", "supercast", "hello_audio", etc.)
import_date UTC datetime the import was run
total_imported Total patron rows created
total_completed Patrons who completed payment setup
total_dropped Patrons who never completed (expired or dropped)
grace_period_days Grace period configured for this import
grace_ends_at UTC datetime when the grace period ends; null if not set
status "active", "complete", or "archived"
imported_by WordPress user ID of the admin who ran the import
notes Admin notes; null if none

Patron fields:

Field Description
id Patron record ID
import_id Links back to an entry in imports
email Email address from the source platform
source_tier Tier or pledge level name on the source platform
mapped_tier_slug The Benecaster tier slug this patron was mapped to
wp_user_id WordPress user ID created or matched during import; null if user was not created
status "imported", "emailed", "clicked", "subscribed", "dropped", or "grace_expired"
invite_sent_at UTC datetime the invitation email was sent; null if not yet sent
completed_at UTC datetime the patron completed payment setup; null if not completed
grace_period_ends_at UTC datetime the grace period expires for this patron; null if no grace period

Note: The migration_records dataset is absent if the migration tables do not exist in the database (i.e. no migration has ever been run). If the tables exist but are empty, the dataset is {"imports": [], "patrons": []}.


Dataset: token_records

An array of raw token table rows. This dataset provides the technical record of every RSS token — including revoked tokens — for audit and data-portability purposes.

The token hash is never included. Tokens are one-way hashed (SHA-256) and the hash is a live credential. The export includes a token_prefix for display/support reference only.

{
  "id": "42",
  "user_id": "391",
  "show_id": "10",
  "token_prefix": "bc_a1b2c3d4",
  "tier_slug": "premium",
  "status": "active",
  "engagement_status": "active",
  "created_at": "2026-01-15 10:30:00",
  "last_accessed_at": "2026-06-10 08:22:41",
  "reset_at": null,
  "revoked_at": null,
  "is_over_limit": "0"
}
Field Description
id Token record ID
user_id WordPress user ID who holds this token
show_id Show this token grants access to
token_prefix First few characters of the token for display/support lookup
tier_slug Feed tier this token grants access to
status "active" or "revoked"
engagement_status Engagement classification based on feed poll frequency
created_at UTC datetime the token was created
last_accessed_at UTC datetime of the most recent feed poll; null if never polled
reset_at UTC datetime the token was last reset; null if never reset
revoked_at UTC datetime the token was revoked; null if active
is_over_limit "1" if this token was flagged as over the subscriber limit; "0" otherwise

Type Note: All fields are JSON strings. Cast numeric IDs and is_over_limit to integers/booleans as needed.


CSV Subscriber Export Format

Encoding and Delimiter

  • Encoding: UTF-8 with BOM (byte order mark EF BB BF) for Excel compatibility
  • Delimiter: Comma (,)
  • Quoting: RFC 4180 double-quote enclosure — any field containing a comma, newline, or double-quote is enclosed in double-quotes; embedded double-quotes are escaped as ""
  • Line endings: CRLF (rn)

Filename

benecaster-subscribers-{show-slug}-{tier-slug-or-all-tiers}-{status}-{YYYY-MM-DD-His}.csv

Examples:

  • benecaster-subscribers-my-podcast-all-tiers-active-2026-06-11-143000.csv — all active subscribers for “my-podcast”
  • benecaster-subscribers-my-podcast-premium-all-2026-06-11-143000.csv — all statuses for the “premium” tier

Columns

The first row is a header. Column order is fixed.

Column Description
email Subscriber’s email address
display_name WordPress display name
tier_slug Internal Benecaster tier slug
tier_name Human-readable tier name
status active or revoked
join_date UTC datetime when the subscriber joined (Y-m-d H:i:s)
grace_period_ends UTC datetime the migration grace period expires; empty if not a migrated subscriber
source_platform Migration source platform (e.g. patreon, supercast); empty if not a migrated subscriber
migrated_from_tier Tier or pledge level name on the source platform; empty if not a migrated subscriber

Example

email,display_name,tier_slug,tier_name,status,join_date,grace_period_ends,source_platform,migrated_from_tier
listener@example.com,Jane Doe,premium,Premium Tier,active,2026-01-15 10:30:00,,,
patron@example.com,John Smith,premium,Premium Tier,active,2026-01-16 11:00:00,2026-02-15 09:00:00,patreon,Gold Patron

Listing and Downloading Files via REST

List Available Exports

GET /wp-json/benecaster/v1/tools/exports
Authorization: X-WP-Nonce {nonce}

Response: array of export objects:

[
  {
    "id": "benecaster-export-2026-06-11-143000",
    "filename": "benecaster-export-2026-06-11-143000.json.gz",
    "file_size": 48291,
    "file_type": "json",
    "created_at": "2026-06-11T14:30:00+00:00",
    "deletes_in_days": 5,
    "download_url": "https://example.com/wp-json/benecaster/v1/tools/exports/benecaster-export-2026-06-11-143000/download"
  }
]

deletes_in_days — days remaining before the nightly sweep unlinks the file. null when the TTL is 0 and auto-deletion is disabled. It can be 0 or negative on a file already past its TTL that the cron has not yet reached — a low-traffic site is the usual reason. Older plugin versions omit the field entirely, so treat its absence as “unknown” rather than “retained”.

Download a File

GET /wp-json/benecaster/v1/tools/exports/{id}/download
Authorization: X-WP-Nonce {nonce}

Returns the file with appropriate Content-Type and Content-Disposition: attachment headers. The caller must be an authenticated WordPress admin (manage_options capability).

Browser download: The download_url returned in the list response is a standard HTTPS URL. If you are logged in to the WordPress admin, you can paste it directly into your browser address bar and the file will download — no curl or REST client needed. The URL requires an active admin session; it will redirect to the login screen if you are not authenticated.

Trigger a New Export

POST /wp-json/benecaster/v1/tools/export
Authorization: X-WP-Nonce {nonce}
Content-Type: application/json

{
  "datasets": ["subscribers", "shows_episodes", "tiers"],
  "show_id": 10
}

datasets accepts any combination of: subscribers, shows_episodes, tiers, migration_records, token_records. Omitting show_id exports all shows.

For sites with fewer than 1,000 active subscribers, the response is synchronous:

{
  "queued": false,
  "id": "benecaster-export-2026-06-11-143000",
  "filename": "benecaster-export-2026-06-11-143000.json.gz",
  "file_size": 48291,
  "created_at": "2026-06-11T14:30:00+00:00",
  "download_url": "..."
}

For sites with 1,000 or more active subscribers, the export is queued via WP-Cron. The admin receives an email when the file is ready. The response is:

{
  "queued": true,
  "job_id": "export-20260611-143000-a1b2c3d4"
}

File Retention

Export files are automatically deleted 7 days after they are generated. A nightly cleanup pass removes any file in the exports directory older than the configured TTL. You do not need to manage this manually — old exports are gone within 24 hours of expiry.

The 7-day default can be changed in two ways:

wp-config.php constant (takes precedence):

define( 'BENECASTER_EXPORT_TTL_DAYS', 14 );

Filter (code-based override, useful for per-show or per-type logic):

add_filter( 'benecaster_export_ttl_days', function( $days ) {
    return 30; // keep for 30 days
} );

Setting the value to 0 disables automatic deletion entirely — useful if you manage export retention through an external backup or archival system. The admin list then shows Retained instead of a countdown.

The admin UI export list shows a Deletes in N days note alongside each file so you can see how much time remains before a file is removed.

The constant short-circuits the filter. When BENECASTER_EXPORT_TTL_DAYS is defined, benecaster_export_ttl_days never runs — the constant is the operator’s hard override and the filter is the developer’s default. If a filter callback appears to do nothing, check for the constant first; that is almost always the explanation.

.htaccess is never deleted. That file is what prevents the exports directory being publicly listable, so the cleanup pass always leaves it in place regardless of the TTL.

Timing is best-effort, not guaranteed. The sweep runs on WP-Cron, which fires on site traffic, so on a low-traffic install files can outlive their stated TTL by a day or more. Treat the countdown as an intention. Where deletion timing is part of a retention commitment, use a real server cron rather than relying on WP-Cron.

Lengthening the window keeps personal data on disk for longer. Exports carry subscriber email addresses and token records, so a longer TTL is a privacy decision that may interact with your published retention policy — not merely a convenience setting.


Format Stability

The JSON export format follows the plugin’s database schema. Fields present in a given version of the export will remain present and semantically stable across minor versions. New fields may be added in future versions.

The CSV subscriber export column list (email, display_name, tier_slug, tier_name, status, join_date, grace_period_ends, source_platform, migrated_from_tier) is considered stable and will not change without a major version bump. Column names are fixed and will not be reordered.

Migration tools should:

  • Parse the benecaster_version field from the envelope and handle any version-specific differences explicitly
  • Treat unknown keys in JSON objects as ignorable rather than errors
  • Not assume a specific column count in CSV files — check headers by name, not position

See Also