Skip to content

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.


All requests run through WordPress’s standard cookie + nonce auth.

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.

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.


bp-stats/v1

Full base URL: {site}/wp-json/bp-stats/v1{route}.


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.

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

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

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
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
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.
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.


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.


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.

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)

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=25
bpStatsApi
.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/reindex
bpStatsApi
.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-log
bpStatsApi
.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.


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' );

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:

  1. Pure data only - no echo, no $_POST, no nonce, no enqueue.
  2. All $wpdb calls use prepare().
  3. Caching is the caller’s job, with one exception. The REST controllers cache service output in transients through Bp_Stats_REST_Controller::cached(). Only Bp_Stats_Widget_Data_Service caches internally, in the bp_stats_widget object-cache group (300 s), so wp_cache_flush_group( 'bp_stats_widget' ) clears it.
  4. Analytics scope. Admin analytics methods on Bp_Stats_Charts_Service resolve bp_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 );

Defined in bp-stats.php. Safe to call from any plugin / theme.

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().

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.

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.

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.