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_versionfield 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
Related
- Exporting Your Data — admin UI for generating exports
- CLI Reference —
wp benecaster exportfor command-line exports