Share links
Pro - CreatorAvailable in the Creator tier
The share link system is only available in Burst Pro.
Share links allow read-only, password-free access to the Burst dashboard for external viewers. A share token is created by an admin, embedded in a path-based URL (/burst-dashboard/{tab}/?burst_share_token={token}), and the recipient opens the dashboard without logging in.
Internally, the system logs in a system user (burst_statistics_viewer) with the burst_viewer role for the duration of the session. REST requests from that session send the token via the X-Burst-Share-Token header.
Share link operations are surfaced through the generic do_action and get_action endpoints.
Share link URLs are path-based (/burst-dashboard/{tab}/?burst_share_token={token}). Hash-based URLs from older share links continue to work — the plugin serves a minimal upgrader page that reads the legacy hash fragment in the browser and redirects to the path-based equivalent. Newly created tokens always return path-based URLs.
Token format
32-character lowercase hexadecimal, generated as bin2hex( random_bytes( 16 ) ). Any value not matching /^[a-f0-9]{32}$/ is rejected without further processing.
Shareable tabs
The list of tab IDs that may appear in shared_tabs is restricted by an internal whitelist:
| Tab ID | Notes |
|---|---|
statistics | Free + Pro |
sources | Free + Pro — the country-level world map is free; campaign and referrer breakdowns require Pro |
sales | Pro (Business tier) |
engagement | Free + Pro |
story | Reserved for report share links — assigned automatically when report_id > 0. Not selectable through shared_tabs |
Any value passed in shared_tabs that is not in the whitelist above is silently dropped during sanitization. dashboard and subscriptions are not shareable.
The engagement tab exposes the reading-engagement and search-terms datatables (Free), plus the forms and outgoing-links datatables (Pro). Each of these datatable IDs is mapped to the engagement tab through the burst_datatable_id_tab_map filter so a share viewer with engagement in shared_tabs can resolve them.
The not-found-pages data endpoints (data/not_found_pages and data/not_found_page_referrers) are mapped to the engagement tab through the burst_endpoint_tab_map filter, so a share viewer with engagement in shared_tabs can resolve the 404 overview and its referrer breakdown.
The sources tab exposes the country-level world map (data/geo) and the locations datatable (sources_countries), both of which are free and mapped to the sources tab from core. Pro extends the same tab with the campaign and referrer breakdowns. A share viewer with sources in shared_tabs can therefore resolve the country data without a Pro license being present.
Request routing
Every share link request is authorized against the Burst endpoint it resolves to. How that endpoint is determined depends on the transport:
| Transport | Endpoint source |
|---|---|
| REST API (dispatched request object available) | WP_REST_Request::get_route() — the route WordPress actually dispatched |
| REST API (no request object passed) | The path segment of REQUEST_URI after burst/v1/ |
AJAX fallback (admin-ajax.php) | The rest_action query parameter or the POST body path, never REQUEST_URI |
During an AJAX fallback request (wp_doing_ajax()), REQUEST_URI is not inspected at all. Burst dispatches those requests from rest_action / path, so the authorization source and the execution source are the same field and cannot diverge — a padded script path such as /wp-admin/admin-ajax.php/burst/v1/data/<granted-endpoint> has no effect on the tab check.
If you build a custom client that talks to Burst over admin-ajax.php, always send the target endpoint in rest_action or in the POST body path. Putting it in the request URI alone resolves to no endpoint and the request is refused for share link sessions.
Data request restrictions
Share-link date and filter restrictions are applied to incoming data request arguments through the burst_get_data_request_args filter at priority 5, before any data class reads $args['date_start'] or $args['filters']. This runs ahead of the ecommerce date filter (priority 10) so the enforced values are the ones seen by raw-SQL callers such as sales, quick-wins and funnel queries. A share viewer therefore cannot widen the date range or change the filter set, even on endpoints that build raw SQL rather than going through the datatable config.
Query scope for share link viewers
A share link viewer is a logged-in burst_viewer user with view_burst_statistics, so Statistics_Query treats it as a trusted, non-strict caller: the full metric catalog, filter keys, group_by and order_by tokens are available, exactly as they are for an administrator. Strict mode applies only to contexts with no logged-in Burst capability — frontend shortcodes and unauthenticated requests.
The scope of what a viewer can actually reach is enforced outside the query builder:
| Layer | What it restricts |
|---|---|
burst_menu (priority PHP_INT_MAX) | Drops every menu item whose id is not in the token's shared_tabs |
burst_endpoint_tab_map | Maps each REST endpoint path to a tab, so only endpoints belonging to a shared tab resolve |
burst_datatable_id_tab_map | Maps each datatable ID to a tab, so only datatables belonging to a shared tab resolve |
App::get_datatable_config() | Every datatable ID declares its own capability — ecommerce tables require view_sales_burst_statistics |
burst_get_data_request_args (priority 5) | Pins the allowed date range and filters onto the request arguments |
| Block resource map (report share links) | Limits a report session to the endpoints and datatable IDs used by the report's blocks |
Do not add a share link check to strict-mode detection. Strict mode narrows the SELECT/WHERE surface but grants no scope of its own; making viewers strict would break legitimate dashboard queries while leaving the layers above as the only thing that actually confines a token.
REST operations
Create or renew a token
Action: do_action/get_share_token
Permission: manage_burst_statistics
Body parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
expiration | string | 7d | Token lifetime: never, 24h, 7d or 30d |
view_url | string | '' | Dashboard URL the share link should open. The path segment determines the initial tab; any legacy hash fragment is parsed and converted to query parameters |
permissions | object | {} | Permission flags — filtered via burst_share_permissions. Valid keys: can_change_date, can_filter |
shared_tabs | array | [] | Tab IDs to expose: statistics, sources, sales, engagement. Other values are dropped during sanitization |
initial_state | object | {} | Pre-selected state: date_range.start, date_range.end, filters (key/value pairs) |
report_id | int | 0 | Attach the token to a specific report. Reuses an existing token for the same report ID if one exists, re-anchoring its expiration to a full window starting now |
view_url is now interpreted as a path-based dashboard URL. Legacy hash routes in the form #/{tab}?{params} are still accepted — the tab segment is moved to the URL path and any query parameters are merged into the real query string of the generated share URL.
Creating the first share token on a site flushes WordPress rewrite rules once so the path-based dashboard routes resolve. This applies to both manually created share links and report tokens generated automatically for scheduled reports.
Response:
Show code
{
"share_token": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"share_url": "https://example.com/burst-dashboard/statistics/?burst_share_token=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"
}
Revoke a token
Action: do_action/revoke_share_link
Permission: manage_burst_statistics
Body parameters:
| Parameter | Type | Description |
|---|---|---|
token | string | The 32-character hex token to revoke |
Response:
Show code
{
"success": true,
"share_links": []
}
List active tokens
Action: do_action/list_share_links
Permission: manage_burst_statistics
This operation moved from get_action/get_share_links to do_action/list_share_links. The burst_get_action handler for share links was removed and the lookup is now performed inside do_rest_action. Update any custom client code calling the previous endpoint.
Returns all active (non-expired) share links and the list of shareable dashboard tabs.
Response:
Show code
{
"share_links": [
{
"token": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"url": "https://example.com/burst-dashboard/statistics/?burst_share_token=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"expires": 1712000000,
"created": 1711913600,
"report_id": 0,
"permissions": { "can_change_date": false, "can_filter": false },
"shared_tabs": ["statistics"],
"initial_state": { "date_range": { "start": "", "end": "" }, "filters": {} }
}
],
"shareable_tabs": [
{ "id": "statistics", "title": "Statistics" }
]
}
The share_links array is always sequentially indexed — filtered results are reindexed before being returned, so JSON encoders emit a true array rather than an object.
Report share links
When a token is created with a non-zero report_id, the generated share URL targets the dedicated story route (/burst-dashboard/story/?burst_share_token={token}) and the corresponding report's to_array() response is scoped to fields safe for external viewers. Scheduling and recipient metadata are withheld.
Report share URLs use the path-based /burst-dashboard/story/ route.
Retrieving a report's token re-anchors its expiration to a full window (the original duration, kept the same, but counted from now instead of from when the token was first created). An expired token is refreshed rather than skipped, so the share URL a scheduled report emails to its recipients stays valid however long after the token was first created the email is sent — recurring reports keep a stable, working link. A token created with never as its expiration is left untouched.
When the first token on a site was auto-generated through a report rather than through the share UI, the rewrite rules may not yet be flushed. In that case the shared tab is resolved directly from the request URI, so the story route still loads the correct tab.
Story report data endpoint
The dedicated get_action/story-report-data endpoint returns the scoped report payload for the current share token. The handler validates the supplied token against validate_share_token() before returning anything and returns an empty array when the token is missing or invalid.
Body parameters:
| Parameter | Type | Description |
|---|---|---|
token | string | The 32-character hex share token tied to the report |
Block-level access control
Report share links enforce a per-block resource map. A request from a report share session is only honored when the resolved REST endpoint or datatable ID is listed under one of the blocks present in the report. Filters defined on a block are also forced onto the request arguments, so a viewer cannot widen the dataset by sending a different filters payload.
The endpoint is resolved as described in Request routing: from the dispatched route object for standard REST requests, falling back to REQUEST_URI when no request object is available, and from rest_action / the POST body path for AJAX fallback requests. Because the AJAX path never reads REQUEST_URI, the endpoint that is authorized is always the endpoint that executes.
The mapping is internal and not filterable. Blocks not in the map have no allowed endpoints, so they cannot be exposed through a share link even if they appear in a report.
Field set returned to share link requests:
| Field | Type | Description |
|---|---|---|
id | int | Report ID |
name | string | Report name |
format | string | Report format identifier |
enabled | bool | Whether the report is active |
content | array | Report content blocks |
reportDateRange | string | Date range for the report |
fixedEndDate | string | Fixed end date if set |
The following admin-only fields are omitted from share link responses: frequency, weekOfMonth, dayOfWeek, sendTime, lastEdit, scheduled, recipients, lastSendStatus, lastSendMessage.
If the report is disabled (enabled = false), to_array() returns an empty array [] for share link requests — the report is not exposed.
lastSendStatus values:
| Value | Description |
|---|---|
ready_to_share | Report is not scheduled; ready to be shared via link. |
sent | Report was successfully sent |
failed | Last send attempt failed |
Legacy URL upgrade
When a request hits /burst-dashboard/?burst_share_token={token} without a tab segment in the path, the plugin returns a small HTML upgrader page instead of loading the React app. That page reads the legacy hash fragment (#/{tab}?{query}) in the browser, validates the tab against the shareable whitelist, and replaces the URL with the path-based equivalent before the dashboard boots.
Old share URLs distributed to recipients therefore continue to work without server-side rewrites — there is no need to manually reissue tokens to existing viewers when upgrading to v3.4.2.
Viewer hardening
The burst_statistics_viewer account is a session-only account: it is signed in through wp_set_auth_cookie() for the duration of a share link visit, and it must never be convertible into a credential that outlives the token. WordPress core lets every logged-in user edit their own profile, so without the guards below a token holder could set a password or an email address on the account and keep logging in after the token is revoked.
The Viewer_Lockdown class registers those guards on every request — frontend, REST, wp-login.php and XML-RPC — because each of those paths can authenticate or edit a user.
What is blocked for the viewer account
| Surface | Filter | Effect |
|---|---|---|
| Application Passwords | wp_is_application_passwords_available_for_user | POST /wp-json/wp/v2/users/me/application-passwords cannot mint a Basic Auth credential that survives token revocation or expiry |
| Self-editing | map_meta_cap on edit_user | POST /wp/v2/users/me, XML-RPC wp.editProfile and profile.php are denied for the viewer editing itself — password, email and profile fields included |
| Username/password login | authenticate (priority 30) | Runs after core's own password checks, so even a correct password is rejected with the burst_viewer_login_disabled error |
| Password reset | allow_password_reset | The lost-password flow never issues a reset key for the account |
The self-edit rule only applies to the viewer editing itself. An administrator editing the viewer account in wp-admin is unaffected, so the account stays manageable.
Share_Auth::reset_viewer_account() restores the account to its generated state: it sets a fresh random password, resets the email address, display name, nickname, first and last name, bio, website and locale, and destroys every session. The password and email change notifications are suppressed, because they would otherwise be sent to an address a recipient may have set.
Viewer user profile
When the burst_statistics_viewer user is created it is given a descriptive bio and a website link pointing to an explanatory article, so an admin who finds the account in wp-admin understands why it exists and that it can be safely left in place. The same descriptive fields are backfilled onto an existing viewer user, filling only empty fields so a manually edited profile is left untouched.
Author archive returns a 404
The viewer account carries an explanatory bio and website link but authors no content. Its author archive — and author enumeration via ?author={id} — would otherwise publicly surface that bio and URL. Burst serves a 404 for the viewer's author archive instead, hooked to template_redirect.
Excluded from public user queries
The viewer user is excluded from every frontend WP_User_Query via pre_get_users, so themes and plugins cannot enumerate the account through author-overview calls such as wp_list_authors( [ 'hide_empty' => false ] ) or a custom get_users() loop. Admin and REST-authenticated listings are left untouched so the account stays manageable in wp-admin.
Viewer session cleanup
A daily cron (burst_daily) destroys every session token belonging to burst_statistics_viewer so viewer sessions never exceed 24 hours regardless of cookie lifetime.
PHP API
Share::tokens->get_share_links()
get_share_links() lives on the Share_Tokens service and is reached through the tokens property on a Share instance.
Show code
$share = burst_loader()->admin->share;
$links = $share->tokens->get_share_links( 'link' ); // only dashboard share links
$links = $share->tokens->get_share_links( 'report', '', 42 ); // links for report ID 42
$links = $share->tokens->get_share_links( 'all' ); // all tokens
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
$type | string | 'all' | 'all' — all tokens; 'link' — dashboard share links (report_id === 0); 'report' — report tokens (report_id !== 0). When $type is 'report' and $report_id > 0, a token is auto-generated if none exists, and an existing report token has its expiration re-anchored to a full window from now (a never token is left as-is) |
$token | string | '' | Optional: filter by a specific token value |
$report_id | int | 0 | Optional: filter by report ID |
Auto-generated report tokens now use the path-based view_url /burst-dashboard/story/ instead of /burst-dashboard/#story.
Retrieving a report token keeps the original lifetime but resets created to now and expires to now plus that lifetime, so the emitted share URL does not expire shortly after a scheduled report is emailed. The refreshed token list is persisted before it is returned.
Filtered results are now passed through array_values() before being returned, so the array is always sequentially indexed regardless of which entries were dropped by the filter. Code that iterated with foreach is unaffected; code that relied on the original numeric keys must be updated.
Share_Auth::reset_viewer_account()
Restores the burst_statistics_viewer account to its generated state and ends all of its sessions.
burst_loader()->admin->share->auth->reset_viewer_account();
Returns: nothing. The call is a no-op when the viewer user does not exist.
Filters
burst_share_permissions
Filters the permissions array applied to a share token before it is stored or returned.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$defaults | array | Default permissions: [ 'can_change_date' => false, 'can_filter' => false ] |
$raw_permissions | array | Raw permissions received from the REST request body |
Show code
add_filter( 'burst_share_permissions', function( array $defaults, array $raw_permissions ): array {
// Merge raw permissions into defaults, keeping only known keys.
return array_merge( $defaults, array_intersect_key( $raw_permissions, $defaults ) );
}, 10, 2 );
burst_share_link_permissions
Returns the resolved permission set for the currently active share link, keyed from the burst_share_token query parameter or X-Burst-Share-Token header in the current request.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$permissions | array | Current permissions array (empty by default when no valid token is present) |
When a valid token is present, the array contains:
| Key | Type | Description |
|---|---|---|
can_change_date | bool | Whether the viewer may change the date range |
can_filter | bool | Whether the viewer may apply filters |
is_shareable_link_viewer | bool | Whether the current user holds the burst_viewer role |
Show code
add_filter( 'burst_share_link_permissions', function( array $permissions ): array {
// Always allow date changes for share link viewers.
if ( ! empty( $permissions['is_shareable_link_viewer'] ) ) {
$permissions['can_change_date'] = true;
}
return $permissions;
} );
burst_endpoint_tab_map
Filters the map of REST endpoint paths to dashboard tabs. A share link request is only honored when the endpoint it resolves to maps to a tab in the token's shared_tabs, so registering a custom endpoint here is what makes it reachable from a share link.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$map | array | Endpoint path (for example data/not_found_pages) mapped to a tab slug |
Show code
add_filter( 'burst_endpoint_tab_map', function( array $map ): array {
$map['data/my_custom_endpoint'] = 'engagement';
return $map;
} );
Endpoints that are not in the map resolve to no tab, and the request is refused for share link sessions.
burst_get_data_request_args (share link context)
Share link enforcement hooks into burst_get_data_request_args at priority 5 to inject the token's allowed date range and filters onto the request arguments before any data class reads them. Because it runs before the ecommerce date filter (priority 10) and before raw-SQL callers (sales, quick-wins, funnel) read $args['date_start'], a share viewer cannot bypass the restrictions by hitting an endpoint that builds SQL directly.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$args | array | Request arguments (date range, filters) |
$type | string | The REST data type being requested |
$request | WP_REST_Request | The current REST request |
burst_verify_nonce (share link context)
Share link authentication hooks into burst_verify_nonce to treat a valid X-Burst-Share-Token header as equivalent to a valid WordPress nonce for burst_viewer users. You can use the same filter to add custom token verification:
Show code
add_filter( 'burst_verify_nonce', function( bool $nonce_is_valid, ?string $nonce, string $action ): bool {
if ( $action === 'burst_nonce' && my_verify_custom_token() ) {
return true;
}
return $nonce_is_valid;
}, 10, 3 );
Parameters:
| Parameter | Type | Description |
|---|---|---|
$nonce_is_valid | bool | Whether the nonce passed standard WordPress verification |
$nonce | string / null | The nonce value from the request |
$action | string | The nonce action string (e.g. burst_nonce) |
burst_menu (share link context)
During share link sessions, burst_menu is applied a second time (at PHP_INT_MAX priority) to strip menu items the token does not grant access to. Only items with 'shareable' => true whose id appears in the token's shared_tabs list are kept.
See Hooks & filters for the full burst_menu documentation.