1"use strict";(self.webpackChunkburst_statistics=self.webpackChunkburst_statistics||[]).push([["7293"],{7668(e,s,n){n.r(s),n.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":"The file is empty on disk (a broken placeholder), so there is no prior body to reproduce verbatim \u2014 I\'ll output a complete authentication doc describing the current behavior, including the re-evaluated share-link authorization, the sharedviewercannot_filter boundary and cron/WP-CLI sales access.","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"}}'),t=n(4848),r=n(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:"Nonces",id:"nonces",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:"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:"Related pages",id:"related-pages",level:2}];function h(e){let s={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(s.p,{children:["The file is empty on disk (a broken placeholder), so there is no prior body to reproduce verbatim \u2014 I'll output a complete authentication doc describing the current behavior, including the re-evaluated share-link authorization, the ",(0,t.jsx)(s.code,{children:"shared_viewer_cannot_filter"})," boundary and cron/WP-CLI sales access."]}),"\n",(0,t.jsx)(s.header,{children:(0,t.jsx)(s.h1,{id:"authentication",children:"Authentication"})}),"\n",(0,t.jsxs)(s.p,{children:["Burst Statistics registers its REST routes under the ",(0,t.jsx)(s.code,{children:"burst/v1"})," namespace. Every route except the public tracking beacon enforces a capability check in its ",(0,t.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,t.jsx)(s.h2,{id:"base-url",children:"Base URL"}),"\n",(0,t.jsx)(s.p,{children:"All routes are served from the WordPress REST API root:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{children:"https://example.com/wp-json/burst/v1/\n"})}),"\n",(0,t.jsxs)(s.p,{children:["The public beacon lives outside the REST API at ",(0,t.jsx)(s.code,{children:"https://example.com/wp-content/plugins/burst-statistics/endpoint.php"}),". See ",(0,t.jsx)(s.a,{href:"/docs/rest-api/tracking-endpoints",children:"Tracking endpoints"})," for that path."]}),"\n",(0,t.jsx)(s.h2,{id:"permission-levels",children:"Permission levels"}),"\n",(0,t.jsxs)(s.p,{children:["Access is expressed through three capabilities. A route's ",(0,t.jsx)(s.code,{children:"permission_callback"})," maps to one of them; it is never ",(0,t.jsx)(s.code,{children:"__return_true"})," for anything that reads or writes statistics data."]}),"\n",(0,t.jsxs)(s.table,{children:[(0,t.jsx)(s.thead,{children:(0,t.jsxs)(s.tr,{children:[(0,t.jsx)(s.th,{children:"Level"}),(0,t.jsx)(s.th,{children:"Capability"}),(0,t.jsx)(s.th,{children:"Who has it"})]})}),(0,t.jsxs)(s.tbody,{children:[(0,t.jsxs)(s.tr,{children:[(0,t.jsx)(s.td,{children:(0,t.jsx)(s.code,{children:"manage"})}),(0,t.jsx)(s.td,{children:(0,t.jsx)(s.code,{children:"manage_burst_statistics"})}),(0,t.jsx)(s.td,{children:"Administrators"})]}),(0,t.jsxs)(s.tr,{children:[(0,t.jsx)(s.td,{children:(0,t.jsx)(s.code,{children:"view"})}),(0,t.jsx)(s.td,{children:(0,t.jsx)(s.code,{children:"view_burst_statistics"})}),(0,t.jsxs)(s.td,{children:["Administrators and ",(0,t.jsx)(s.code,{children:"burst_viewer"})," role holders accessing a share link"]})]}),(0,t.jsxs)(s.tr,{children:[(0,t.jsx)(s.td,{children:(0,t.jsx)(s.code,{children:"view_sales"})}),(0,t.jsx)(s.td,{children:(0,t.jsx)(s.code,{children:"view_sales_burst_statistics"})}),(0,t.jsxs)(s.td,{children:["Pro only; granted separately from ",(0,t.jsx)(s.code,{children:"view"}),". Share-link viewers require a sales-capable tab in the token"]})]})]})]}),"\n",(0,t.jsxs)(s.p,{children:["The generic beacon and ",(0,t.jsx)(s.code,{children:"burst/v1/track"})," route are public and require no capability."]}),"\n",(0,t.jsx)(s.h2,{id:"authentication-mechanisms",children:"Authentication mechanisms"}),"\n",(0,t.jsx)(s.h3,{id:"cookie-plus-nonce",children:"Cookie plus nonce"}),"\n",(0,t.jsxs)(s.p,{children:["The Burst admin dashboard authenticates with the logged-in WordPress session cookie and sends a ",(0,t.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,t.jsx)(s.code,{children:"X-WP-Nonce"})," header, or \u2014 for the dashboard's proxied ",(0,t.jsx)(s.code,{children:"do_action"})," / ",(0,t.jsx)(s.code,{children:"get_action"})," calls \u2014 in the request body as ",(0,t.jsx)(s.code,{children:"nonce"}),"."]}),"\n",(0,t.jsx)(s.h3,{id:"application-password",children:"Application Password"}),"\n",(0,t.jsxs)(s.p,{children:["Server-to-server integrations authenticate with a ",(0,t.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,t.jsx)(s.pre,{children:(0,t.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,t.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,t.jsx)(s.h3,{id:"share-token",children:"Share token"}),"\n",(0,t.jsxs)(s.p,{children:["Share links authenticate a read-only viewer without a password. The token travels in the ",(0,t.jsx)(s.code,{children:"X-Burst-Share-Token"})," header (or the ",(0,t.jsx)(s.code,{children:"burst_share_token"})," query parameter on the dashboard URL). The plugin logs in the ",(0,t.jsx)(s.code,{children:"burst_statistics_viewer"})," system user with the ",(0,t.jsx)(s.code,{children:"burst_viewer"})," role for the duration of the request. Application Passwords are disabled for that account."]}),"\n",(0,t.jsxs)(s.p,{children:["Share links are a Pro feature and are documented in full under ",(0,t.jsx)(s.a,{href:"/docs/rest-api/share-links",children:"Share links"}),"."]}),"\n",(0,t.jsx)(s.admonition,{type:"pro",proTier:"pro",children:(0,t.jsxs)(s.p,{children:["Share links are available in ",(0,t.jsx)(s.a,{href:"https://burst-statistics.com/pricing/",children:"Burst Pro"}),"."]})}),"\n",(0,t.jsx)(s.h3,{id:"mainwp-signature",children:"MainWP signature"}),"\n",(0,t.jsxs)(s.p,{children:["The ",(0,t.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,t.jsx)(s.admonition,{type:"pro",proTier:"agency",children:(0,t.jsxs)(s.p,{children:["The MainWP integration is available in the ",(0,t.jsx)(s.a,{href:"https://burst-statistics.com/pricing/",children:"Agency tier"}),"."]})}),"\n",(0,t.jsx)(s.h2,{id:"nonces",children:"Nonces"}),"\n",(0,t.jsxs)(s.p,{children:["State-changing routes verify a ",(0,t.jsx)(s.code,{children:"burst_nonce"})," action nonce through ",(0,t.jsx)(s.code,{children:"wp_verify_nonce"}),". The verified result is passed through the ",(0,t.jsx)(s.code,{children:"burst_verify_nonce"})," filter, so integrations can extend nonce handling. A request that fails nonce verification receives a ",(0,t.jsx)(s.code,{children:"403"})," response and is not dispatched."]}),"\n",(0,t.jsxs)(s.p,{children:["Nonce verification is bypassed only when the current request is authenticated by HTTP Basic Auth and WordPress has fired ",(0,t.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,t.jsx)(s.h2,{id:"share-link-authorization",children:"Share-link authorization"}),"\n",(0,t.jsxs)(s.p,{children:["A share token is scoped to a set of tabs (",(0,t.jsx)(s.code,{children:"shared_tabs"}),") and a set of permission flags. Beyond the capability check, two further boundaries apply to share-link viewers."]}
1),"\n",(0,t.jsx)(s.h3,{id:"authorization-follows-the-dispatched-route",children:"Authorization follows the dispatched route"}),"\n",(0,t.jsxs)(s.p,{children:["For a share-link viewer, ",(0,t.jsx)(s.code,{children:"view"})," and ",(0,t.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,t.jsx)(s.code,{children:"do_action"})," / ",(0,t.jsx)(s.code,{children:"get_action"}),", where the target endpoint can be named in ",(0,t.jsx)(s.code,{children:"rest_action"})," (query string) and again in a JSON ",(0,t.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,t.jsxs)(s.p,{children:[(0,t.jsx)(s.code,{children:"user_can_view()"})," and ",(0,t.jsx)(s.code,{children:"user_can_view_sales()"})," therefore receive the dispatched ",(0,t.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,t.jsx)(s.code,{children:"rest_action"})," and a non-granted endpoint in the POST ",(0,t.jsx)(s.code,{children:"path"}),": both must resolve to a tab the token permits, and any ",(0,t.jsx)(s.code,{children:"burst/v1/data/ecommerce/"})," route additionally requires a sales-capable tab."]}),"\n",(0,t.jsxs)(s.p,{children:["The registered ",(0,t.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,t.jsx)(s.h3,{id:"filter-dependent-handlers-deny-unfiltered-viewers",children:"Filter-dependent handlers deny unfiltered viewers"}),"\n",(0,t.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,t.jsx)(s.code,{children:"can_filter"})," boundary. A share-link viewer whose token does not grant ",(0,t.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,t.jsxs)(s.p,{children:["See ",(0,t.jsx)(s.a,{href:"/docs/rest-api/share-links",children:"Share links"})," for the ",(0,t.jsx)(s.code,{children:"can_filter"})," and ",(0,t.jsx)(s.code,{children:"can_change_date"})," permission flags, and ",(0,t.jsx)(s.a,{href:"/docs/rest-api/filters-reference",children:"Filters reference"})," for the filter set enforced on share-link data requests."]}),"\n",(0,t.jsx)(s.h2,{id:"cron-and-wp-cli",children:"Cron and WP-CLI"}),"\n",(0,t.jsxs)(s.p,{children:["Sales access (",(0,t.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,t.jsx)(s.code,{children:"view_sales"})," from the capability and the token's tabs as described above."]}),"\n",(0,t.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,t.jsx)(s.h2,{id:"related-pages",children:"Related pages"}),"\n",(0,t.jsxs)(s.ul,{children:["\n",(0,t.jsxs)(s.li,{children:[(0,t.jsx)(s.a,{href:"/docs/rest-api/",children:"REST API overview"})," \u2014 endpoint list and permission summary"]}),"\n",(0,t.jsxs)(s.li,{children:[(0,t.jsx)(s.a,{href:"/docs/rest-api/admin-endpoints",children:"Admin endpoints"})," \u2014 dashboard routes, ",(0,t.jsx)(s.code,{children:"do_action"})," and ",(0,t.jsx)(s.code,{children:"get_action"})]}),"\n",(0,t.jsxs)(s.li,{children:[(0,t.jsx)(s.a,{href:"/docs/rest-api/data-endpoints",children:"Data endpoints"})," \u2014 general analytics data"]}),"\n",(0,t.jsxs)(s.li,{children:[(0,t.jsx)(s.a,{href:"/docs/rest-api/ecommerce-endpoints",children:"Ecommerce endpoints"})," \u2014 revenue and sales data"]}),"\n",(0,t.jsxs)(s.li,{children:[(0,t.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,t.jsx)(s,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},8453(e,s,n){n.d(s,{R:()=>a,x:()=>d});var i=n(6540);let t={},r=i.createContext(t);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(t):e.components||t: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.