PageSourceSearch

https://burst-statistics.com/docs/assets/js/4d8bfc71.430c8a25.js

js burst-statistics.com collected 2026-10-02 16:32:07 UTC 26,473 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunkburst_statistics=self.webpackChunkburst_statistics||[]).push([["7293"],{7668(e,s,t){t.r(s),t.d(s,{metadata:()=>i,default:()=>l,frontMatter:()=>a,contentTitle:()=>d,toc:()=>c,assets:()=>o});var i=JSON.parse('{"id":"rest-api/authentication","title":"Authentication","description":"Burst Statistics registers its REST routes under the burst/v1 namespace. Every route except the public tracking beacon enforces a capability check in its permission_callback, and every state-changing request additionally verifies a nonce. This page describes the authentication mechanisms, the permission levels behind them and how access is resolved for share-link viewers.","source":"@site/docs/04-rest-api/01-authentication.md","sourceDirName":"04-rest-api","slug":"/rest-api/authentication","permalink":"/docs/rest-api/authentication","draft":false,"unlisted":false,"tags":[],"version":"current","sidebarPosition":1,"frontMatter":{},"sidebar":"docs","previous":{"title":"Overview","permalink":"/docs/rest-api/"},"next":{"title":"Admin endpoints","permalink":"/docs/rest-api/admin-endpoints"}}'),n=t(4848),r=t(8453);let a={},d="Authentication",o={},c=[{value:"Base URL",id:"base-url",level:2},{value:"Permission levels",id:"permission-levels",level:2},{value:"Authentication mechanisms",id:"authentication-mechanisms",level:2},{value:"Cookie plus nonce",id:"cookie-plus-nonce",level:3},{value:"Application Password",id:"application-password",level:3},{value:"Share token",id:"share-token",level:3},{value:"MainWP signature",id:"mainwp-signature",level:3},{value:"Viewer account lockdown",id:"viewer-account-lockdown",level:2},{value:"Checking the lockdown from code",id:"checking-the-lockdown-from-code",level:3},{value:"Nonces",id:"nonces",level:2},{value:"Capability requirements per endpoint group",id:"capability-requirements-per-endpoint-group",level:2},{value:"Export downloads",id:"export-downloads",level:2},{value:"Share-link authorization",id:"share-link-authorization",level:2},{value:"Authorization follows the dispatched route",id:"authorization-follows-the-dispatched-route",level:3},{value:"The dispatched query follows the authorized action",id:"the-dispatched-query-follows-the-authorized-action",level:3},{value:"Filter-dependent handlers deny unfiltered viewers",id:"filter-dependent-handlers-deny-unfiltered-viewers",level:3},{value:"Cron and WP-CLI",id:"cron-and-wp-cli",level:2},{value:"Troubleshooting",id:"troubleshooting",level:2},{value:"Related pages",id:"related-pages",level:2}];function h(e){let s={a:"a",admonition:"admonition",code:"code",details:"details",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",p:"p",pre:"pre",strong:"strong",summary:"summary",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(s.header,{children:(0,n.jsx)(s.h1,{id:"authentication",children:"Authentication"})}),"\n",(0,n.jsxs)(s.p,{children:["Burst Statistics registers its REST routes under the ",(0,n.jsx)(s.code,{children:"burst/v1"})," namespace. Every route except the public tracking beacon enforces a capability check in its ",(0,n.jsx)(s.code,{children:"permission_callback"}),", and every state-changing request additionally verifies a nonce. This page describes the authentication mechanisms, the permission levels behind them and how access is resolved for share-link viewers."]}),"\n",(0,n.jsx)(s.h2,{id:"base-url",children:"Base URL"}),"\n",(0,n.jsx)(s.p,{children:"All routes are served from the WordPress REST API root:"}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{children:"https://example.com/wp-json/burst/v1/\n"})}),"\n",(0,n.jsxs)(s.p,{children:["The public beacon lives outside the REST API at ",(0,n.jsx)(s.code,{children:"https://example.com/wp-content/plugins/burst-statistics/endpoint.php"}),". See ",(0,n.jsx)(s.a,{href:"/docs/rest-api/tracking-endpoints",children:"Tracking endpoints"})," for that path."]}),"\n",(0,n.jsx)(s.h2,{id:"permission-levels",children:"Permission levels"}),"\n",(0,n.jsxs)(s.p,{children:["Access is expressed through three capabilities. A route's ",(0,n.jsx)(s.code,{children:"permission_callback"})," maps to one of them; it is never ",(0,n.jsx)(s.code,{children:"__return_true"})," for anything that reads or writes statistics data."]}),"\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Level"}),(0,n.jsx)(s.th,{children:"Capability"}),(0,n.jsx)(s.th,{children:"Who has it"})]})}),(0,n.jsxs)(s.tbody,{children:[(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"manage"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"manage_burst_statistics"})}),(0,n.jsx)(s.td,{children:"Administrators"})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"view"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"view_burst_statistics"})}),(0,n.jsxs)(s.td,{children:["Administrators and ",(0,n.jsx)(s.code,{children:"burst_viewer"})," role holders accessing a share link"]})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"view_sales"})}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"view_sales_burst_statistics"})}),(0,n.jsxs)(s.td,{children:["Pro only; granted separately from ",(0,n.jsx)(s.code,{children:"view"}),". Share-link viewers require a sales-capable tab in the token"]})]})]})]}),"\n",(0,n.jsxs)(s.p,{children:["The generic beacon and ",(0,n.jsx)(s.code,{children:"burst/v1/track"})," route are public and require no capability."]}),"\n",(0,n.jsx)(s.h2,{id:"authentication-mechanisms",children:"Authentication mechanisms"}),"\n",(0,n.jsx)(s.h3,{id:"cookie-plus-nonce",children:"Cookie plus nonce"}),"\n",(0,n.jsxs)(s.p,{children:["The Burst admin dashboard authenticates with the logged-in WordPress session cookie and sends a ",(0,n.jsx)(s.code,{children:"burst_nonce"})," nonce with every request. Read requests resolve the capability from the cookie; write requests additionally require a valid nonce. Pass the nonce in the ",(0,n.jsx)(s.code,{children:"X-WP-Nonce"})," header, or \u2014 for the dashboard's proxied ",(0,n.jsx)(s.code,{children:"do_action"})," / ",(0,n.jsx)(s.code,{children:"get_action"})," calls \u2014 in the request body as ",(0,n.jsx)(s.code,{children:"nonce"}),"."]}),"\n",(0,n.jsx)(s.h3,{id:"application-password",children:"Application Password"}),"\n",(0,n.jsxs)(s.p,{children:["Server-to-server integrations authenticate with a ",(0,n.jsx)(s.a,{href:"https://developer.wordpress.org/rest-api/frequently-asked-questions/#how-can-i-authenticate-requests",children:"WordPress Application Password"})," over HTTP Basic Auth. No browser session is involved, so no nonce is required: when the request is authenticated by an Application Password, the nonce check is skipped."]}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-bash",children:'curl "https://example.com/wp-json/burst/v1/data/pages?date_start=2026-08-01&date_end=2026-08-18" \\\n    --user "admin_user:xxxx xxxx xxxx xxxx xxxx xxxx"\n'})}),"\n",(0,n.jsx)(s.p,{children:"The nonce skip is scoped to the request that actually carries the HTTP Basic Authorization header, so a later cookie-authenticated call in the same PHP request still has its nonce verified. Create an Application Password under Users \u2192 Profile \u2192 Application Passwords for a user who holds the required capability."}),"\n",(0,n.jsx)(s.h3,{id:"share-token",children:"Share token"}),"\n",(0,n.jsxs)(s.p,{children:["Share links authenticate a read-only viewer without a password. The token travels in the ",(0,n.jsx)(s.code,{children:"X-Burst-Share-Token"})," header (or the ",(0,n.jsx)(s.code,{children:"burst_share_token"})," query parameter on the dashboard URL). The plugin logs in the ",(0,n.jsx)(s.code,{children:"burst_statistics_viewer"})," system user with the ",(0,n.jsx)(s.code,{children:"burst_viewer"})," role for the duration of the request."]}),"\n",(0,n.jsxs)(s.p,{children:["That account is session-only: it exists to carry the share-link session and can never become a standalone login. See ",(0,n.jsx)(s.a,{href:"#viewer-account-lockdown",children:"Viewer account lockdown"})," for the exact restrictions."]}),"\n",(0,n.jsxs)(s.p,{children:["Share links are a Pro feature and are documented in full under ",(0,n.jsx)(s.a,{href:"/docs/rest-api/share-links",children:"Share links"}),"."]}),"\n",(0,n.jsx)(s.admonition,{type:"pro",proTier:"pro",children:(0,n.jsxs)(s.p,{children:["Share links are available in ",(0,n.jsx)(s.a,{href:"https://burst-statistics.com/pricing/",children:"Burst Pro"}),"."]})}),"\n",(0,n.jsx)(s.h3,{id:"mainwp-signature",children:"MainWP signature"}),"\n",(0,n.jsxs)(s.p,{children:["The ",(0,n.jsx)(s.code,{children:"burst/v1/mainwp-auth"})," route authenticates a MainWP dashboard request with an RSA signature rather than a capability, then issues a short-lived Application Password for subsequent calls. This path is only used by the MainWP integration."]}),"\n",(0,n.jsx)(s.admonition,{type:"pro",proTier:"agency",children:(0,n.jsxs)(s.p,{children:["The MainWP integration is available in the ",(0,n.jsx)(s.a,{href:"https://burst-statistics.com/pricing/",children:"Agency tier"}),"."]})}),"\n",(0,n.jsx)(s.h2,{id:"viewer-account-lockdown",children:"Viewer account lockdown"}),"\n",(0,n.jsxs)(s.p,{children:["The ",(0,n.jsx)(s.code,{children:"burst_statistics_viewer"})," account is authenticated through ",(0,n.jsx)(s.code,{children:"wp_set_auth_cookie()"})," when a valid share token is presented. WordPress core lets every logged-in user edit their own profile, so without extra restrictions a share-link recipient could turn that temporary session into a password or an Application Password and keep access after the token is revoked. Burst therefore denies four things for any user holding the ",(0,n.jsx)(s.code,{children:"burst_viewer"})," role:"]}),"\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Restriction"}),(0,n.jsx)(s.th,{children:"Filter"}),(0,n.jsx)(s.th,{children:"Effect"})]})}),(0,n.jsxs)(s.tbody,{children:[(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Application Passwords"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"wp_is_application_passwords_available_for_user"})}),(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.code,{children:"POST /wp/v2/users/me/application-passwords"})," cannot mint a credential for the viewer"]})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Self-edit"}),(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.code,{children:"map_meta_cap"})," (",(0,n.jsx)(s.code,{children:"edit_user"})," on self)"]}),(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.code,{children:"POST /wp/v2/users/me"}),", XML-RPC ",(0,n.jsx)(s.code,{children:"wp.editProfile"})," and ",(0,n.jsx)(s.code,{children:"profile.php"})," are denied, so password, email and profile fields stay fixed"]})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Password login"}),(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.code,{children:"authenticate"})," (priority 30)"]}),(0,n.jsx)(s.td,{children:"A username/password login for the viewer is rejected even when the password is correct"})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.td,{children:"Password reset"}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"allow_password_reset"})}),(0,n.jsx)(s.td,{children:"The lost-password flow never issues a reset key for the viewer"})]})]})]}),"\n",(0,n.jsxs)(s.p,{children:["These filters are registered on every request \u2014 frontend, REST, ",(0,n.jsx)(s.code,{children:"wp-login.php"})," and XML-RPC \u2014 because each of those paths can authenticate or edit a user. Administrators editing the viewer account are unaffected: the self-edit rule only applies to the viewer editing itself."]}),"\n",(0,n.jsx)(s.p,{children:"Statistics access itself still depends on a valid share token; the lockdown makes sure the session cannot outlive it."}),"\n",(0,n.jsx)(s.h3,{id:"checking-the-lockdown-from-code",children:"Checking the lockdown from code"}),"\n",(0,n.jsx)(s.p,{children:"The restrictions are plain WordPress filters, so a capability check is enough to confirm them:"}),"\n",(0,n.jsxs)(s.details,{className:"code-collapse",children:[(0,n.jsx)(s.summary,{children:"Show code"}),(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{className:"language-php",children:"$viewer = get_user_by( 'login', 'burst_statistics_viewer' );
1\n\n// False: the viewer may not edit its own profile.\n$can_self_edit = user_can( $viewer, 'edit_user', $viewer->ID );\n\n// False: Application Passwords are unavailable for the viewer.\n$can_use_app_passwords = wp_is_application_passwords_available_for_user( $viewer );\n"})})]}),"\n",(0,n.jsxs)(s.p,{children:["Both calls return ",(0,n.jsx)(s.code,{children:"false"})," on a site running Burst. An administrator checking ",(0,n.jsx)(s.code,{children:"user_can( $admin, 'edit_user', $viewer->ID )"})," still gets ",(0,n.jsx)(s.code,{children:"true"}),"."]}),"\n",(0,n.jsx)(s.h2,{id:"nonces",children:"Nonces"}),"\n",(0,n.jsxs)(s.p,{children:["State-changing routes verify a ",(0,n.jsx)(s.code,{children:"burst_nonce"})," action nonce through ",(0,n.jsx)(s.code,{children:"wp_verify_nonce"}),". The verified result is passed through the ",(0,n.jsx)(s.code,{children:"burst_verify_nonce"})," filter, so integrations can extend nonce handling. A request that fails nonce verification receives a ",(0,n.jsx)(s.code,{children:"403"})," response and is not dispatched."]}),"\n",(0,n.jsxs)(s.p,{children:["Nonce verification is bypassed only when the current request is authenticated by HTTP Basic Auth and WordPress has fired ",(0,n.jsx)(s.code,{children:"application_password_did_authenticate"})," \u2014 i.e. a genuine Application Password request, where a CSRF nonce would be redundant."]}),"\n",(0,n.jsx)(s.h2,{id:"capability-requirements-per-endpoint-group",children:"Capability requirements per endpoint group"}),"\n",(0,n.jsxs)(s.p,{children:["Read routes that serve statistics data require ",(0,n.jsx)(s.code,{children:"view_burst_statistics"})," (or ",(0,n.jsx)(s.code,{children:"view_sales_burst_statistics"})," for ecommerce data). Endpoints that administer the installation rather than read from it require ",(0,n.jsx)(s.code,{children:"manage_burst_statistics"}),":"]}),"\n",(0,n.jsxs)(s.table,{children:[(0,n.jsx)(s.thead,{children:(0,n.jsxs)(s.tr,{children:[(0,n.jsx)(s.th,{children:"Endpoint group"}),(0,n.jsx)(s.th,{children:"Required capability"})]})}),(0,n.jsxs)(s.tbody,{children:[(0,n.jsxs)(s.tr,{children:[(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.code,{children:"data/*"}),", ",(0,n.jsx)(s.code,{children:"data/datatable/{id}"})]}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"view_burst_statistics"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsxs)(s.td,{children:[(0,n.jsx)(s.code,{children:"data/ecommerce/*"}),", ",(0,n.jsx)(s.code,{children:"data/ecommerce/datatable/{id}"})]}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"view_sales_burst_statistics"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsxs)(s.td,{children:["Import routes (",(0,n.jsx)(s.code,{children:"import/upload/init"}),", ",(0,n.jsx)(s.code,{children:"import/upload/chunk"}),", ",(0,n.jsx)(s.code,{children:"import/upload/finalize"}),", and the ",(0,n.jsx)(s.code,{children:"start_import"})," / ",(0,n.jsx)(s.code,{children:"import_chunk"})," / ",(0,n.jsx)(s.code,{children:"cancel_import"})," / ",(0,n.jsx)(s.code,{children:"rollback_import"})," / ",(0,n.jsx)(s.code,{children:"clear_import_history"})," / ",(0,n.jsx)(s.code,{children:"import_progress"})," / ",(0,n.jsx)(s.code,{children:"import_sources_status"})," / ",(0,n.jsx)(s.code,{children:"import_history"})," actions)"]}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"manage_burst_statistics"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsxs)(s.td,{children:["Export actions (",(0,n.jsx)(s.code,{children:"export_start"}),", ",(0,n.jsx)(s.code,{children:"export_chunk"}),", ",(0,n.jsx)(s.code,{children:"export_pause"}),", ",(0,n.jsx)(s.code,{children:"export_cancel"}),", ",(0,n.jsx)(s.code,{children:"export_delete"}),", ",(0,n.jsx)(s.code,{children:"export_save_schedule"}),", ",(0,n.jsx)(s.code,{children:"export_status"}),")"]}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"manage_burst_statistics"})})]}),(0,n.jsxs)(s.tr,{children:[(0,n.jsxs)(s.td,{children:["Tour actions (",(0,n.jsx)(s.code,{children:"tour_progress"}),", ",(0,n.jsx)(s.code,{children:"tour_start"}),", ",(0,n.jsx)(s.code,{children:"tour_resume"}),", ",(0,n.jsx)(s.code,{children:"tour_dismiss"}),", ",(0,n.jsx)(s.code,{children:"tour_reset"}),", ",(0,n.jsx)(s.code,{children:"tour_complete_feature"}),", ",(0,n.jsx)(s.code,{children:"tour_steps"}),")"]}),(0,n.jsx)(s.td,{children:(0,n.jsx)(s.code,{children:"manage_burst_statistics"})})]})]})]}),"\n",(0,n.jsxs)(s.p,{children:["Import and export handlers exchange server file paths, run detection queries against third-party plugin tables and write to the statistics tables, so a view capability is not enough for them \u2014 including for their read actions such as ",(0,n.jsx)(s.code,{children:"import_history"})," and ",(0,n.jsx)(s.code,{children:"export_status"}),"."]}),"\n",(0,n.jsx)(s.h2,{id:"export-downloads",children:"Export downloads"}),"\n",(0,n.jsxs)(s.p,{children:["A finished export is downloaded from ",(0,n.jsx)(s.code,{children:"admin-
1post.php"}),", not from the REST API:"]}),"\n",(0,n.jsx)(s.pre,{children:(0,n.jsx)(s.code,{children:"https://example.com/wp-admin/admin-post.php?action=burst_export_download&export_id={id}&_wpnonce={nonce}\n"})}),"\n",(0,n.jsxs)(s.p,{children:["The download URL is generated per export by ",(0,n.jsx)(s.code,{children:"Export_Manager::get_download_url()"})," and carries a nonce bound to that export ID (",(0,n.jsx)(s.code,{children:"burst_export_download_{export_id}"}),"). The handler checks ",(0,n.jsx)(s.code,{children:"manage_burst_statistics"})," and verifies the nonce against the requested export ID before streaming the file; a request with a missing, expired or mismatched nonce receives a ",(0,n.jsx)(s.code,{children:"403"}),". A nonce minted for one export is therefore not valid for another."]}),"\n",(0,n.jsx)(s.h2,{id:"share-link-authorization",children:"Share-link authorization"}),"\n",(0,n.jsxs)(s.p,{children:["A share token is scoped to a set of tabs (",(0,n.jsx)(s.code,{children:"shared_tabs"}),") and a set of permission flags. Beyond the capability check, two further boundaries apply to share-link viewers."]}),"\n",(0,n.jsx)(s.h3,{id:"authorization-follows-the-dispatched-route",children:"Authorization follows the dispatched route"}),"\n",(0,n.jsxs)(s.p,{children:["For a share-link viewer, ",(0,n.jsx)(s.code,{children:"view"})," and ",(0,n.jsx)(s.code,{children:"view_sales"})," access is not global \u2014 it depends on which tab the requested endpoint belongs to. The dashboard proxies data requests through ",(0,n.jsx)(s.code,{children:"do_action"})," / ",(0,n.jsx)(s.code,{children:"get_action"}),", where the target endpoint can be named in ",(0,n.jsx)(s.code,{children:"rest_action"})," (query string) and again in a JSON ",(0,n.jsx)(s.code,{children:"path"})," in the POST body. Because the body value can override the query-string action after the initial check, the permission decision is re-evaluated against the endpoint the request is actually about to dispatch."]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.code,{children:"user_can_view()"})," and ",(0,n.jsx)(s.code,{children:"user_can_view_sales()"})," therefore receive the dispatched ",(0,n.jsx)(s.code,{children:"WP_REST_Request"})," and resolve the share token's allowed tab from the same action that selects the data. A one-tab share viewer cannot name an allowed endpoint in ",(0,n.jsx)(s.code,{children:"rest_action"})," and a non-granted endpoint in the POST ",(0,n.jsx)(s.code,{children:"path"}),": both must resolve to a tab the token permits, and any ",(0,n.jsx)(s.code,{children:"burst/v1/data/ecommerce/"})," route additionally requires a sales-capable tab."]}),"\n",(0,n.jsxs)(s.p,{children:["The registered ",(0,n.jsx)(s.code,{children:"permission_callback"})," on each granular route also receives the dispatched request, so the same tab check applies whether a route is called directly or through the proxy."]}),"\n",(0,n.jsx)(s.h3,{id:"the-dispatched-query-follows-the-authorized-action",children:"The dispatched query follows the authorized action"}),"\n",(0,n.jsxs)(s.p,{children:["When the REST API is unreachable, the dashboard falls back to ",(0,n.jsx)(s.code,{children:"admin-ajax.php"}),". In that path the endpoint is named by the ",(0,n.jsx)(s.code,{children:"rest_action"})," query parameter and may be overridden by a ",(0,n.jsx)(s.code,{children:"path"})," value in the POST body; the authoritative action is the one that survives that override."]}),"\n",(0,n.jsxs)(s.p,{children:["The ",(0,n.jsx)(s.code,{children:"type"})," parameter that selects which query runs is derived from that single authoritative action (",(0,n.jsx)(s.code,{children:"App::resolve_fallback_data_type()"}),"), never from ",(0,n.jsx)(s.code,{children:"rest_action"})," parsed separately. Authorization and dispatch therefore always describe the same endpoint: there is no combination of ",(0,n.jsx)(s.code,{children:"rest_action"})," and POST ",(0,n.jsx)(s.code,{children:"path"})," that authorizes one endpoint while a different one executes. A ",(0,n.jsx)(s.code,{children:"data/ecommerce/"})," action also sets ",(0,n.jsx)(s.code,{children:"is_ecommerce"})," on the dispatched request from that same resolution."]}),"\n",(0,n.jsxs)(s.p,{children:["During an ",(0,n.jsx)(s.code,{children:"admin-ajax.php"})," request the share-token routing never parses ",(0,n.jsx)(s.code,{children:"REQUEST_URI"})," to determine the endpoint. On that path the request URI is not the REST route \u2014 the script name can carry trailing ",(0,n.jsx)(s.code,{children:"PATH_INFO"})," \u2014 so the endpoint is resolved only from the Burst request fields (",(0,n.jsx)(s.code,{children:"rest_action"})," and the POST ",(0,n.jsx)(s.code,{children:"path"}),"). Standard REST requests, where the URI is the route, continue to resolve from ",(0,n.jsx)(s.code,{children:"REQUEST_URI"}),"."]}),"\n",(0,n.jsx)(s.h3,{id:"filter-dependent-handlers-deny-unfiltered-viewers",children:"Filter-dependent handlers deny unfiltered viewers"}),"\n",(0,n.jsxs)(s.p,{children:["Handlers that return filter-dependent resources \u2014 the advanced filter options list and the goals list \u2014 apply an independent ",(0,n.jsx)(s.code,{children:"can_filter"})," boundary. A share-link viewer whose token does not grant ",(0,n.jsx)(s.code,{children:"can_filter"})," is denied by these handlers regardless of how execution reached them, so access can never silently depend on whichever route happened to dispatch. Non-shared users (for example an administrator with a view capability) are unaffected."]}),"\n",(0,n.jsxs)(s.p,{children:["See ",(0,n.jsx)(s.a,{href:"/docs/rest-api/share-links",children:"Share links"})," for the ",(0,n.jsx)(s.code,{children:"can_filter"})," and ",(0,n.jsx)(s.code,{children:"can_change_date"})," permission flags, and ",(0,n.jsx)(s.a,{href:"/docs/rest-api/filters-reference",children:"Filters reference"})," for the filter set enforced on share-link data requests."]}),"\n",(0,n.jsx)(s.h2,{id:"cron-and-wp-cli",children:"Cron and WP-CLI"}),"\n",(0,n.jsxs)(s.p,{children:["Sales access (",(0,n.jsx)(s.code,{children:"view_sales"}),") is granted automatically to WP-Cron and WP-CLI callers, since those contexts run scheduled aggregation and command-line reporting that need revenue data but carry no logged-in user. Cookie and share-token requests still resolve ",(0,n.jsx)(s.code,{children:"view_sales"})," from the capability and the token's tabs as described above."]}),"\n",(0,n.jsx)(s.p,{children:"Note that this load-time convenience is not a substitute for a per-request capability check on state-changing endpoints: those still verify their own capability and nonce."}),"\n",(0,n.jsx)(s.h2,{id:"troubleshooting",children:"Troubleshooting"}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsx)(s.strong,{children:"A share-link viewer cannot change its own password or email."}
1)," That is the lockdown working as designed. The viewer account is not meant to be used as a login; to give someone a real account, create a normal WordPress user and grant ",(0,n.jsx)(s.code,{children:"view_burst_statistics"}),"."]}),"\n",(0,n.jsxs)(s.p,{children:[(0,n.jsxs)(s.strong,{children:["An integration authenticating as the viewer account gets a ",(0,n.jsx)(s.code,{children:"403"})," on ",(0,n.jsx)(s.code,{children:"/wp/v2/users/me/application-passwords"}),"."]})," Application Passwords are disabled for the ",(0,n.jsx)(s.code,{children:"burst_viewer"})," role. Create the Application Password on an administrator account instead, and use a share link only for read-only dashboard access."]}),"\n",(0,n.jsx)(s.h2,{id:"related-pages",children:"Related pages"}),"\n",(0,n.jsxs)(s.ul,{children:["\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.a,{href:"/docs/rest-api/",children:"REST API overview"})," \u2014 endpoint list and permission summary"]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.a,{href:"/docs/rest-api/admin-endpoints",children:"Admin endpoints"})," \u2014 dashboard routes, ",(0,n.jsx)(s.code,{children:"do_action"})," and ",(0,n.jsx)(s.code,{children:"get_action"})]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.a,{href:"/docs/rest-api/data-endpoints",children:"Data endpoints"})," \u2014 general analytics data"]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.a,{href:"/docs/rest-api/ecommerce-endpoints",children:"Ecommerce endpoints"})," \u2014 revenue and sales data"]}),"\n",(0,n.jsxs)(s.li,{children:[(0,n.jsx)(s.a,{href:"/docs/rest-api/share-links",children:"Share links"})," \u2014 read-only external access"]}),"\n"]})]})}function l(e={}){let{wrapper:s}={...(0,r.R)(),...e.components};return s?(0,n.jsx)(s,{...e,children:(0,n.jsx)(h,{...e})}):h(e)}},8453(e,s,t){t.d(s,{R:()=>a,x:()=>d});var i=t(6540);let n={},r=i.createContext(n);function a(e){let s=i.useContext(r);return i.useMemo(function(){return"function"==typeof e?e(s):{...s,...e}},[s,e])}function d(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:a(e.components),i.createElement(r.Provider,{value:s},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.