PHP / REST API Reference
This document is the canonical HTTP contract for the BuddyPress Statistics plugin. As of 2.0.0, the plugin’s data lives in pure-data services and is exposed over REST. The REST surface (bp-stats/v1) is the stable extension point - service classes are listed at the bottom as an internal API for plugin extenders, but their method signatures may change between minor versions.
For the underlying architecture, see Plugin Architecture.
Authentication
Section titled “Authentication”All requests run through WordPress’s standard cookie + nonce auth.
Frontend / Admin (in-WP) calls
Section titled “Frontend / Admin (in-WP) calls”Send the cookie automatically (browser does this for same-origin requests) and pass the WP REST nonce as X-WP-Nonce. The plugin localizes the nonce + root URL on every admin page and on every BP profile / group page where it loads its scripts:
window.bpStatsRest = { root: 'https://example.com/wp-json/bp-stats/v1', nonce: 'abcd1234ef'};The shipped JS wrapper assets/js/api-fetch.js reads this global and falls back to WordPress’s own window.wpApiSettings if it is loaded outside the plugin’s own pages.
External / headless callers
Section titled “External / headless callers”Use Application Passwords and send Authorization: Basic <base64(user:app_pass)>. The same permission callbacks apply - manage_options for /admin/*, group role membership for /groups/*, etc.
Namespace
Section titled “Namespace”bp-stats/v1Full base URL: {site}/wp-json/bp-stats/v1{route}.
Endpoint Catalogue
Section titled “Endpoint Catalogue”43 routes (44 route + method pairs, because /admin/activity-log answers both GET and DELETE) across 6 controllers. Path parameters such as {user_id} are listed in the route, not in the Args column. The canonical inventory is audit/manifest.json#/rest.
/admin/* - Admin (cap: manage_options)
Section titled “/admin/* - Admin (cap: manage_options)”Meta + collections
Section titled “Meta + collections”| Route | Method | Args | Returns | Handler | Replaces |
|---|---|---|---|---|---|
/admin/groups |
GET | search |
List of {id, name} |
Bp_Stats_REST_Admin::get_groups |
wp_ajax_bp_stats_get_group |
/admin/member-types |
GET | - | Member-type slugs + labels | Bp_Stats_REST_Admin::get_member_types |
wp_ajax_bp_stats_get_member_type |
/admin/user-roles |
GET | - | WP roles | Bp_Stats_REST_Admin::get_user_roles |
wp_ajax_bp_stats_get_user_role |
/admin/xprofile/fields/{field_id} |
GET | - | xProfile field options | Bp_Stats_REST_Admin::get_xprofile_field_options |
wp_ajax_bp_stats_xprofile_option |
/admin/users/search |
GET | q |
[{id, label}] (filterable via bp_stats_rest_search_users) |
Bp_Stats_REST_Admin::search_users |
wp_ajax_bp_stats_json_search_users |
/admin/users/column-visibility |
POST | column (required), visible (bool, required) |
Updated visibility map (stored in the bp_stats_toggle_column option) |
Bp_Stats_REST_Admin::toggle_column_visibility |
wp_ajax_bp_stats_toggle_column |
Dashboard summary + sub-widgets
Section titled “Dashboard summary + sub-widgets”period accepts day, week (default) or month.
| Route | Method | Args | Handler |
|---|---|---|---|
/admin/dashboard/summary |
GET | period |
Bp_Stats_REST_Admin_Charts::get_dashboard_summary |
/admin/dashboard/top-groups |
GET | period, limit (1-50, default 5) |
Bp_Stats_REST_Admin_Charts::get_top_groups |
/admin/dashboard/active-users |
GET | period |
Bp_Stats_REST_Admin_Charts::get_active_users |
/admin/dashboard/registered-users |
GET | period |
Bp_Stats_REST_Admin_Charts::get_registered_users |
Chart endpoints
Section titled “Chart endpoints”On the Bp_Stats_REST_Admin_Charts routes, filter is a date-range token: today, yesterday, last-7days (default), last-14days, last-30days, last-90days, last-180days, this-week, this-month, this-year, all-time.
| Route | Method | Args | Handler |
|---|---|---|---|
/admin/charts/engagement |
GET | filter |
Bp_Stats_REST_Admin_Charts::get_engagement |
/admin/charts/engagement/trends |
GET | filter |
Bp_Stats_REST_Admin_Charts::get_engagement_trends |
/admin/charts/user-growth |
GET | filter |
Bp_Stats_REST_Admin_Charts::get_user_growth |
/admin/charts/users/{user_id}/activity |
GET | filter (user_id 0 = all users) |
Bp_Stats_REST_Admin_Charts::get_user_activity |
/admin/charts/groups/{group_id}/activity |
GET | filter, date |
Bp_Stats_REST_Admin_Charts::get_group_activity |
/admin/charts/member-types |
GET | - | Bp_Stats_REST_Admin::get_member_type_chart |
/admin/charts/xprofile/{field_id} |
GET | - | Bp_Stats_REST_Admin::get_xprofile_field_chart |
/admin/charts/xprofile/{field_id}/multi |
GET | - | Bp_Stats_REST_Admin::get_xprofile_multi_field_chart |
Pulse + Lifecycle (since 2.0.0)
Section titled “Pulse + Lifecycle (since 2.0.0)”| Route | Method | Args | Handler | Purpose |
|---|---|---|---|---|
/admin/pulse |
GET | period (day, week, month, quarter, year) |
Bp_Stats_REST_Admin_Charts::get_pulse |
Community Pulse block on Overview |
/admin/contributors/lifecycle |
GET | period (week, month, quarter), limit (1-25) |
Bp_Stats_REST_Admin_Charts::get_contributor_lifecycle |
Champions / Rising / Fading triptych |
Activity Log
Section titled “Activity Log”| Route | Method | Args | Notes |
|---|---|---|---|
/admin/activity-log |
GET | page, per_page (1-100), search, date_from, date_to, topic, orderby (id, date, user, topic, action), order (asc, desc), draw |
Paginated list with X-WP-Total headers. When draw is present the response uses the DataTables shape ({draw, recordsTotal, recordsFiltered, data}) and the handler reads DataTables’ own start, length, order[0][...] and search[value] params, so order also accepts the DataTables sort array. |
/admin/activity-log |
DELETE | - | Truncate the log (Reset). Returns 204 No Content. |
/admin/activity-log/reindex |
POST | types[] (required, at least one), offset, limit (1-100, default 50) |
Historical reindex; the JS caller pages through in batches. |
/admin/activity-log/recent |
GET | limit (1-50, default 10) |
Recent N entries for the Overview tile. Handler: Bp_Stats_REST_Admin::get_recent_logs. |
Per-user / per-group admin extras
Section titled “Per-user / per-group admin extras”| Route | Method | Args | Handler |
|---|---|---|---|
/admin/users/{user_id}/contributions |
GET | filter (7days, monthly, quarterly, yearly), date |
Bp_Stats_REST_Admin::get_user_contributions |
/admin/groups/{group_id}/contributors |
GET | filter (daily, weekly, monthly, quarterly, yearly), date |
Bp_Stats_REST_Admin::get_group_contributors |
date is an optional ISO anchor date (YYYY-MM-DD) on every route that takes it.
/profile/{user_id}/* - Member profile (auth: logged-in + bp_stats_can_view_user_stats)
Section titled “/profile/{user_id}/* - Member profile (auth: logged-in + bp_stats_can_view_user_stats)”| Route | Method | Args | Returns | Service method |
|---|---|---|---|---|
/profile/{user_id}/activity |
GET | filter (weekly, monthly), date |
Chart series | Bp_Stats_Profile_Service::get_activity |
/profile/{user_id}/engagement |
GET | filter, date |
Chart series | Bp_Stats_Profile_Service::get_engagement |
/profile/{user_id}/messages |
GET | filter, date |
Chart series | Bp_Stats_Profile_Service::get_messages |
/profile/{user_id}/group-participation |
GET | filter, date |
Chart series | Bp_Stats_Profile_Service::get_group_participation |
/profile/{user_id}/journey |
GET | - | Hero ribbon - Journey card (since 2.0.0) | Bp_Stats_Journey_Service::get_journey |
/profile/{user_id}/impact |
GET | period (week, month, quarter, year) |
Hero ribbon - Impact card (since 2.0.0) | Bp_Stats_Journey_Service::get_impact |
/profile/{user_id}/streak |
GET | - | Hero ribbon - Streak card (since 2.0.0) | Bp_Stats_Journey_Service::get_streak |
/profile/{user_id}/groups |
GET | - | Groups this user belongs to | Bp_Stats_Profile_Service::get_groups_overview |
The Bp_Stats_REST_Controller::permission_member callback rejects unauthenticated requests, a missing user_id, and any caller for whom bp_stats_can_view_user_stats( $user_id ) is false.
/groups/{group_id}/* - Group page (auth: logged-in + group role gate)
Section titled “/groups/{group_id}/* - Group page (auth: logged-in + group role gate)”The permission_group callback requires is_user_logged_in(). Site administrators (manage_options) are then allowed through, matching templates/group-reports.php. Everyone else must be a member of the group with a role (admin / mod / member) that the site owner enabled in the Display settings tab.
| Route | Method | Args | Service method |
|---|---|---|---|
/groups/{group_id}/activity |
GET | filter (daily, weekly, monthly), date |
Bp_Stats_Groups_Service::get_activity |
/groups/{group_id}/growth |
GET | filter, date |
Bp_Stats_Groups_Service::get_growth |
/groups/{group_id}/top-contributors |
GET | limit (1-100, default 10) |
Bp_Stats_Groups_Service::get_top_contributors |
/groups/{group_id}/contributors |
GET | limit |
Alias of /top-contributors |
/groups/{group_id}/activity-trend |
GET | days (1-90, default 30) |
Bp_Stats_Groups_Service::get_activity_trend |
/groups/{group_id}/member-growth |
GET | days (1-90, default 30) |
Bp_Stats_Groups_Service::get_member_growth_trend |
/groups/{group_id}/engagement-rate |
GET | days (1-365, default 30) |
Bp_Stats_Groups_Service::get_engagement_rate |
/widgets/* - Sidebar widgets (auth: logged-in)
Section titled “/widgets/* - Sidebar widgets (auth: logged-in)”None of the widget routes are public. Anonymous requests get bp_stats_rest_unauthorized.
| Route | Method | Args | Permission | Replaces |
|---|---|---|---|---|
/widgets/modern/{user_id} |
GET | category (logged_in_user, other_user), filter (all, 7, 30, 90) |
permission_member |
wp_ajax_bp_stats_modern_widget_data |
/widgets/sparkline/{user_id} |
GET | days (1-90, default 30) |
permission_member |
wp_ajax_bp_stats_widget_sparkline_data |
/widgets/charts/{type} |
GET | type = member_activities | group_activities, user_id (member charts), group_id (group charts), filter |
permission_chart_widget: member charts need bp_stats_can_view_user_stats; group charts need group membership, a public group, or manage_options |
wp_ajax_bp_stats_load_chart_widget |
The chart widget logic lives in Bp_Stats_REST_Widgets::get_chart_widget(). The old Bp_Stats_Public::bp_stats_load_chart_widget_callback() is a deprecated stub kept for third-party code that called it directly; nothing hooks it.
Argument Validation
Section titled “Argument Validation”Every enum argument (filter, period, orderby, order, type) is registered with 'validate_callback' => 'rest_validate_request_arg' (since 2.0.1), so WordPress checks the value against the declared enum before the handler runs. A value outside the list returns 400 with code rest_invalid_param instead of silently falling back to a default:
GET /wp-json/bp-stats/v1/admin/charts/engagement?filter=last-2days-> 400 {"code":"rest_invalid_param","message":"Invalid parameter(s): filter", ...}The one exception is order on GET /admin/activity-log: it accepts asc / desc or an array, because the DataTables client sends its own order[0][column] / order[0][dir] structure. Integer args (limit, days, per_page) are not registered with a validate_callback, so their minimum / maximum are not enforced by WordPress; they pass through absint and any bounds the handler applies.
Response Conventions
Section titled “Response Conventions”Success
Section titled “Success”A 200 OK response with a JSON body. There is no shared chart envelope: each route returns the shape its Chart.js consumer reads, so check the handler before relying on a key. A few examples:
| Route | Top-level keys |
|---|---|
/admin/charts/user-growth |
labels, values |
/admin/charts/engagement |
activities, comments, messages, period |
/profile/{user_id}/activity |
total_activities, total_comments, series |
/groups/{group_id}/activity |
labels, activities, comments, likes, totals |
/widgets/charts/{type} |
keys, values |
GET /admin/activity-log (REST-native shape) sets X-WP-Total and X-WP-TotalPages headers, plus Link: <next>; rel="next" / <prev>; rel="prev", through Bp_Stats_REST_Controller::paginated_response(). It is currently the only paginated collection route.
Failure
Section titled “Failure”A WP_Error is returned with the appropriate HTTP status. Permission failures use rest_authorization_required_code(), which is 401 for a logged-out caller and 403 for a logged-in one.
| Code | HTTP | When |
|---|---|---|
bp_stats_rest_unauthorized |
401 | Anonymous request to a profile, group or widget route |
bp_stats_rest_forbidden |
401 / 403 | Cap check failed (e.g. manage_options), role gate failed, or bp_stats_can_view_user_stats denied |
bp_stats_rest_invalid_user |
400 | user_id missing / 0 |
bp_stats_rest_invalid_group |
400 | group_id missing / 0 |
bp_stats_rest_invalid_type |
400 | Unknown chart widget type (normally caught earlier by enum validation) |
bp_stats_rest_invalid_types |
400 | POST /admin/activity-log/reindex with no types |
bp_stats_rest_search_empty |
400 | GET /admin/users/search with an empty q |
bp_stats_rest_field_not_found |
404 | xProfile field missing or has no options |
bp_stats_rest_no_groups / bp_stats_rest_no_xprofile |
503 | The Groups or xProfile component is not active |
rest_invalid_param |
400 | Standard WP REST arg validation failure, including an enum value outside the allowed list (see Argument Validation) |
JS Client - bpStatsApi
Section titled “JS Client - bpStatsApi”Every admin / public JS file that previously called admin-ajax.php now uses the wrapper in assets/js/api-fetch.js. The wrapper is plain native fetch - no jQuery, no axios - and exposes .get(), .post(), .delete(), plus a .url() helper and the current .nonce() (so libraries like DataTables that own the fetch themselves can read it).
// GET /bp-stats/v1/admin/activity-log?per_page=25bpStatsApi .get( '/admin/activity-log', { per_page: 25, page: 1 } ) .then( body => console.log( body ) ) .catch( err => console.error( err.message ) );
// POST /bp-stats/v1/admin/activity-log/reindexbpStatsApi .post( '/admin/activity-log/reindex', { types: [ 'index-activities', 'index-groups' ], offset: 0, limit: 50 } ) .then( body => { // Envelope: { success, data: { finished, progress_percent, processed_count, next_offset, details, message } } if ( body.data.finished ) { console.log( 'Reindex complete' ); } else { console.log( 'Next offset:', body.data.next_offset ); } } );
// DELETE /bp-stats/v1/admin/activity-logbpStatsApi .delete( '/admin/activity-log' ) .then( () => console.log( 'Log truncated' ) );
// Build a URL for DataTables$('#log-table').dataTable( { ajax: { url: bpStatsApi.url( '/admin/activity-log', { draw: 1 } ), beforeSend: xhr => xhr.setRequestHeader( 'X-WP-Nonce', bpStatsApi.nonce() ) }} );On a 200 OK the promise resolves with the parsed JSON body (or null for 204 No Content). On failure it rejects with the JSON error body - {code, message, data: {status}} - so a .catch( err => err.message ) pattern works.
Database Table
Section titled “Database Table”The plugin owns one custom table: {prefix}bp_stats_activity_log. Schema in PLUGIN_ARCHITECTURE.md → Database.
If you need to read the table directly from your own plugin code, always go through $wpdb->prepare() and ideally consume the service layer instead:
// Same data as GET /admin/charts/engagement?filter=last-30days$series = Bp_Stats_Charts_Service::get_engagement( 'last-30days' );Service Layer (internal API)
Section titled “Service Layer (internal API)”The six service classes in includes/services/ are the plugin’s internal extension surface for plugin extenders. They are stable enough to be called from a companion plugin, but their method signatures may change between minor versions - pin your code against BP_STATS_VERSION if you depend on them. The REST surface is the stable contract.
| Class | File | Owns |
|---|---|---|
Bp_Stats_Charts_Service |
class-bp-stats-charts-service.php |
Admin dashboard + engagement + user-growth + retention + user-health-segments |
Bp_Stats_Groups_Service |
class-bp-stats-groups-service.php |
The 7 frontend group endpoints |
Bp_Stats_Journey_Service |
class-bp-stats-journey-service.php |
Hero ribbon (journey / impact / streak) |
Bp_Stats_Profile_Service |
class-bp-stats-profile-service.php |
Member profile chart endpoints |
Bp_Stats_Pulse_Service |
class-bp-stats-pulse-service.php |
Community Pulse + Champions/Rising/Fading lifecycle |
Bp_Stats_Widget_Data_Service |
class-bp-stats-widget-data-service.php |
Sidebar widget queries (single-group, all-groups summary, modern member) |
All service methods are static except the group-widget methods on Bp_Stats_Widget_Data_Service (get_single_group_stats(), get_all_groups_overview(), get_top_groups(), count_groups(), get_groups_activity_type_breakdown()), which need an instance. Bp_Stats_Widget_Data_Service::get_modern_member_widget() is static.
Services have a strict contract:
- Pure data only - no
echo, no$_POST, no nonce, no enqueue. - All
$wpdbcalls useprepare(). - Caching is the caller’s job, with one exception. The REST controllers cache service output in transients through
Bp_Stats_REST_Controller::cached(). OnlyBp_Stats_Widget_Data_Servicecaches internally, in thebp_stats_widgetobject-cache group (300 s), sowp_cache_flush_group( 'bp_stats_widget' )clears it. - Analytics scope. Admin analytics methods on
Bp_Stats_Charts_Serviceresolvebp_stats_get_user_scope_ids()and add scoped cache keys; see Hooks & Filters - Analytics Scope.
Example - call the widget service directly:
$service = new Bp_Stats_Widget_Data_Service();$stats = $service->get_single_group_stats( 42 );
// Served from the bp_stats_widget object-cache group for 5 minutes$stats = $service->get_single_group_stats( 42 );Top-Level Helper Functions
Section titled “Top-Level Helper Functions”Defined in bp-stats.php. Safe to call from any plugin / theme.
bp_stats_get_user_last_login( $user_id )
Section titled “bp_stats_get_user_last_login( $user_id )”Returns a translated relative string (“Just now”, “5 minutes ago”, “3 days ago”), or an empty string if the user has no recorded login. Read from the last_login user-meta key written by Bp_Stats_Login_Tracker::record_login().
bp_stats_get_user_ip( $anonymize = null )
Section titled “bp_stats_get_user_ip( $anonymize = null )”Returns the current request’s IP address, or an empty string if it is invalid. If $anonymize is null (default) it reads the anonymize_ip_addresses setting; pass true / false to force. Output is filtered through bp_stats_get_user_ip.
bp_stats_can_view_user_stats( $user_id = 0 )
Section titled “bp_stats_can_view_user_stats( $user_id = 0 )”Returns bool. The same helper called by Bp_Stats_REST_Controller::permission_member. Administrators always pass; otherwise the displayed user must hold a role enabled in show_user_roles, and a viewer other than that user is decided by the bp_stats_can_view_user_stats filter (default false).
bp_stats_get_user_scope_ids( $context ) (since 2.0.1)
Section titled “bp_stats_get_user_scope_ids( $context ) (since 2.0.1)”Returns the sorted user IDs that admin analytics queries are restricted to, array( 0 ) when a filter returned a non-empty value with no valid ID (scope to nobody), or null when unscoped. Resolves the bp_stats_user_scope_ids filter; see Hooks & Filters - Analytics Scope.
bp_stats_general_default_settings()
Section titled “bp_stats_general_default_settings()”Returns the default settings array used as the second arg to get_option( 'bp_stats_general_settings', $default ). Filterable via bp_stats_general_default_settings.
bp_stats_icon( $name, $args = array() )
Section titled “bp_stats_icon( $name, $args = array() )”Returns pre-escaped Lucide icon markup (an <i> element). $args accepts size (sm, md, lg, xl), class and label (empty label marks the icon decorative). Replaces the hand-rolled SVG strings from 1.8.0.
bp_stats_is_buddyboss() / bp_stats_is_buddyboss_reactions_enabled()
Section titled “bp_stats_is_buddyboss() / bp_stats_is_buddyboss_reactions_enabled()”Boolean probes for the BuddyBoss Platform + its Reactions component.
Related Documentation
Section titled “Related Documentation”- Plugin Architecture
- Hooks & Filters
- CSS Design System
- Canonical endpoint inventory:
audit/manifest.json#/rest

