Groups API

New in version 0.9.80

The Groups module provides a comprehensive API for managing cohorts, teams, and group-based access. This guide covers the public API functions, hooks, and filters available for extending the Groups functionality.

Group Roles, Relationships, and Target-User Visibility in 1.1.0+

Since version 1.1.0, Groups is also the general relationship layer for member-to-member visibility. The default roles owner, leader, and member still exist, but admins can define custom site-wide group roles with labels, ranks, capabilities, and data scopes. Normal member and invite flows validate role slugs through GroupRoleDefinitionService, and owner is not assigned through regular member/invite UI.

Use GroupMemberRelationshipService for explicit viewer-to-target assignments, and use GroupRolePermissionService or UserDataVisibilityPermissionService before rendering another member’s learning data. Bricks dynamic tags and read-only elements can resolve the target user from Bricks user loops, including User loops filtered by Users: Group Membership, profile pages, then the current user fallback.

Checking Module Status

Before using Groups APIs, verify the module is active:

use BaselMedia\BricksMembers\Core\ModuleRegistry;

if ( ModuleRegistry::is_active( 'groups' ) ) {
    // Groups functionality available
}

Helper Functions

GroupContextReader::get_instance()->get_user_active_group_for_post()

Get the user’s active group ID for a specific post context.

/**
 * @param int $user_id User ID
 * @param int $post_id Post ID
 * @return int|null Group ID or null if no active group
 */
$group_id = \BaselMedia\BricksMembers\Modules\Groups\GroupContextReader::get_instance()
    ->get_user_active_group_for_post( $user_id, $post_id );

brm_get_user_groups()

Get all groups a user belongs to.

/**
 * @param int   $user_id User ID
 * @param array $args    Optional: status, role filters
 * @return array Array of group arrays
 */
$groups = brm_get_user_groups( $user_id, array( 'status' => 'active' ) );

brm_get_user_groups() is the only public procedural Groups wrapper. For owned internal reads, use GroupService::get_group(), GroupService::is_group_member( $user_id, $group_id ), GroupService::get_user_group_role( $user_id, $group_id ), and GroupContextReader::group_matches_post_required_levels() or get_user_active_group_for_post().

GroupService Class

For more complex operations, use the GroupService singleton:

use BaselMedia\BricksMembers\Modules\Groups\GroupService;

$service = GroupService::get_instance();

Creating Groups

$group_id = $service->create_group( array(
    'name'           => 'Spring 2026 Cohort',
    'owner_user_id'  => get_current_user_id(),
    'group_type'     => 'cohort',
    'total_seats'    => 25,
    'default_levels' => array( 1, 2 ),
    'starts_at'      => '2026-04-01 00:00:00',
    'expires_at'     => '2026-07-01 00:00:00',
) );

if ( is_wp_error( $group_id ) ) {
    // Handle error
}

Updating Groups

$result = $service->update_group( $group_id, array(
    'name'        => 'Updated Name',
    'total_seats' => 30,
) );

Archiving Groups

$service->archive_group( $group_id );

Member Management

// Add a member
$membership_id = $service->add_member( $group_id, $user_id, 'member' );

// Remove a member
$service->remove_member( $group_id, $user_id );

// Change role
$service->change_member_role( $group_id, $user_id, 'leader' );

// Transfer member to another group
$service->transfer_member( $user_id, $from_group_id, $to_group_id );

Getting Members

// Get all members
$members = $service->get_group_members( $group_id, array(
    'status' => 'active',
    'role'   => 'member', // or any assignable custom role slug
) );

// Get a specific membership
$membership = $service->get_membership( $group_id, $user_id );

InvitationService Class

use BaselMedia\BricksMembers\Modules\Groups\InvitationService;

$invitation_service = InvitationService::get_instance();

Creating Invitations

// Email invitation (single-use, specific recipient)
$invitation_id = $invitation_service->create_email_invitation(
    $group_id,
    'student@example.com',
    get_current_user_id(),
    array(
        'role'       => 'member',
        'expires_at' => '2026-04-01 00:00:00',
    )
);

// Initial delivery is automatic through the Group Invitation Created
// automation when that workflow exists. Without that workflow, call the
// plaintext fallback explicitly.
if ( ! $invitation_service->is_invitation_email_sent_by_creation_automation() ) {
    $invitation_service->send_invitation_email( $invitation_id );
}

// Join link (reusable, public)
$link_id = $invitation_service->create_join_link(
    $group_id,
    get_current_user_id(),
    array(
        'role'       => 'member',
        // For limited groups: must be <= current available seats.
        // If omitted, defaults to available seats.
        'max_uses'   => 10,
        'expires_at' => '2026-04-01 00:00:00',
    )
);

// Get the shareable URL
$invitation = $invitation_service->get_invitation( $link_id );
$url = $invitation_service->get_claim_url( $invitation['token'] );

Validating and Claiming

// Validate before claiming
$validation = $invitation_service->validate_invitation( $token, $user_id );
if ( is_wp_error( $validation ) ) {
    echo $validation->get_error_message();
    return;
}

// Claim the invitation
$membership_id = $invitation_service->claim_invitation( $token, $user_id );

Managing Invitations

// Get by token
$invitation = $invitation_service->get_invitation_by_token( $token );

// Get all for a group
$invitations = $invitation_service->get_group_invitations( $group_id, array(
    'status' => 'pending',
    'type'   => 'email',   // or 'link'
) );

// Delete permanently
$invitation_service->delete_invitation( $invitation_id );

// Resend email. When an automation owns delivery, this re-dispatches the
// Group Invitation Created trigger instead of sending plaintext fallback.
$invitation_service->send_invitation_email( $invitation_id, true );

SeatRuleService Class

For purchase matching from native Payments, WooCommerce, FluentCart, SureCart, or generic webhooks:

use BaselMedia\BricksMembers\Modules\Groups\SeatRuleService;

$seat_service = SeatRuleService::get_instance();

// Create a rule
$rule_id = $seat_service->create_rule( array(
    'product_ref'     => 'team-license-pro',
    'provider'        => 'webhook',
    'group_type'      => 'team',
    'levels_to_grant' => array( 1, 2 ),
    'total_seats'     => 5,
    'create_mode'     => 'per_purchase',
    'fulfillment_mode' => 'auto_create_group',
) );

// Process a purchase. This records a seat entitlement and creates or tops up
// the buyer's group according to the rule fulfillment mode.
$result = $seat_service->process_purchase(
    'team-license-pro', // product_ref
    '',                 // variation_ref
    $owner_user_id,     // who purchased
    'order-123',        // unique reference
    '',                 // optional initial group name
    '',                 // optional group start date
    'webhook',          // source
    1                   // purchase quantity
);

// $result contains: entitlement_id, group_id, seats_granted, fulfillment_mode

GroupSeatPoolService Class

GroupSeatPoolService is the canonical owner for purchase-backed group seat entitlements and post-purchase allocation. Seat rules decide whether a purchase matches; this service records the idempotent purchase ledger in wp_brm_group_seat_entitlements, creates or tops up the buyer’s group, reads owner pool summaries, and moves seats between owned groups.

use BaselMedia\BricksMembers\Modules\Groups\GroupSeatPoolService;

$pool_service = GroupSeatPoolService::get_instance();

$summary = $pool_service->get_pool_summary( $owner_user_id, $seat_rule_id );

$new_group_id = $pool_service->create_group_from_pool(
    $owner_user_id,
    $seat_rule_id,
    'Acme Sales Team',
    5,
    $source_group_id // optional: split movable seats from an existing owned group
);

$pool_service->move_group_seats( $owner_user_id, $source_group_id, $target_group_id, 2 );

Allocation methods require the current owner relationship, keep groups within the same seat_rule_id, and block any reduction below used_seats + reserved_seats. Purchase fulfillment and allocation mutations use a per-buyer/per-rule concurrency lock.

Member Chat Integration

When the Member Chat module is enabled, Groups becomes the lifecycle owner for primary group rooms.

  • brm_event_group_created is used to create or resolve the primary group room
  • brm_event_group_member_added synchronizes the new user into the room
  • brm_event_group_member_removed removes the user from the group’s chat rooms
  • brm_event_group_archived archives the group’s related chat rooms

The current primary-room contract is one room per group with scope = group and room_kind = primary_group in the Member Chat tables.

Actions (Hooks)

Group Lifecycle

// After group is created
add_action( 'brm_event_group_created', function( \BaselMedia\BricksMembers\Core\Event $event ) {
    $context = $event->get_context();
}, 10, 1 );

// After group is archived
add_action( 'brm_event_group_archived', function( \BaselMedia\BricksMembers\Core\Event $event ) {
    $group_id = $event->get_resource_id();
}, 10, 1 );

Member Lifecycle

// After member joins
add_action( 'brm_event_group_member_added', function( \BaselMedia\BricksMembers\Core\Event $event ) {
    $user_id  = $event->get_user_id();
    $group_id = $event->get_resource_id();
    $role     = $event->get_context()['role'] ?? '';
}, 10, 1 );

// After member leaves/removed
add_action( 'brm_event_group_member_removed', function( \BaselMedia\BricksMembers\Core\Event $event ) {
    $user_id  = $event->get_user_id();
    $group_id = $event->get_resource_id();
}, 10, 1 );

// After member transfer
add_action( 'brm_event_group_member_transferred', function( \BaselMedia\BricksMembers\Core\Event $event ) {
    $context = $event->get_context();
}, 10, 1 );

Invitation Lifecycle

// After invitation created
add_action( 'brm_invitation_created', function( $invitation_id, $group_id, $email, $invited_by ) {
    // Custom notifications, logging
}, 10, 4 );

// After invitation claimed
add_action( 'brm_invitation_claimed', function( $invitation_id, $user_id, $group_id, $invitation ) {
    // Welcome sequence, analytics
}, 10, 4 );

// After invitation email sent
add_action( 'brm_invitation_email_sent', function( $invitation_id, $invitation ) {
    // Track plaintext fallback sends and automation-backed resend dispatches
}, 10, 2 );

Filters

Customizing Invitation Emails

// Custom subject
add_filter( 'brm_invitation_email_subject', function( $subject, $invitation, $group ) {
    return sprintf( 'Join %s - Exclusive Invitation', $group['name'] );
}, 10, 3 );

// Custom message body
add_filter( 'brm_invitation_email_message', function( $message, $invitation, $group, $claim_url ) {
    return "You've been personally invited to join {$group['name']}.\n\n"
         . "Click here to accept: {$claim_url}\n\n"
         . "This is a limited opportunity!";
}, 10, 4 );

Customizing Claim Redirects

Since 1.1.0, the global invitation redirect defaults and built-in claim notice toggle are owned by the Groups admin Settings tab, not the general Settings screen.

For normal site setup, configure the default guest redirect page, post-claim redirect page, and built-in claim notice in BricksMembers → Groups → Settings. Use the group detail Settings tab for per-group overrides. The filters below are for custom code that needs to override the resolved destination.

// After successful claim
add_filter( 'brm_invitation_claim_success_redirect', function( $url, $group_id, $user_id ) {
    return home_url( '/welcome-to-group/?group=' . $group_id );
}, 10, 3 );

// After failed claim
add_filter( 'brm_invitation_claim_error_redirect', function( $url, $error, $token ) {
    return home_url( '/invitation-error/?code=' . $error->get_error_code() );
}, 10, 3 );

Customizing Claim URL Base

add_filter( 'brm_invitation_claim_base_url', function( $url ) {
    return home_url( '/join/' ); // Custom join page
} );

Bricks Integration

Custom Conditions

Register custom Bricks conditions based on groups:

add_filter( 'bricks/conditions/options', function( $options ) {
    $options[] = array(
        'key'     => 'brm_custom_group_condition',
        'label'   => 'Custom Group Check',
        'group'   => 'brm',
        'compare' => array(
            '='  => 'equals',
            '!=' => 'not equals',
        ),
        'value'   => array(
            'type'   => 'text',
            'placeholder' => 'Group type',
        ),
    );
    return $options;
}, 15 );

add_filter( 'bricks/conditions/result', function( $result, $key, $condition ) {
    if ( 'brm_custom_group_condition' !== $key ) {
        return $result;
    }

    $group_id = \BaselMedia\BricksMembers\Modules\Groups\GroupContextReader::get_instance()
        ->get_user_active_group_for_post( get_current_user_id(), get_the_ID() );
    if ( ! $group_id ) {
        return false;
    }

    $group = \BaselMedia\BricksMembers\Modules\Groups\GroupService::get_instance()
        ->get_group( (int) $group_id );
    $compare_value = $condition['value'] ?? '';
    $compare = $condition['compare'] ?? '=';

    $matches = $group['group_type'] === $compare_value;
    return '=' === $compare ? $matches : ! $matches;
}, 10, 3 );

Group Context and Demo Preview

Group dynamic tags and conditions are not limited to brmUserGroupsQuery rows. They resolve from the current BRM group context, which can come from a group query row, an explicit dashboard link/group request context, invitation claim state, or the current user’s matching active group for the queried post. If the viewer has only one matching active group, tags such as {brm_group:name} can resolve on a normal dashboard page without a visible BRM query loop.

Dashboard demo context is owned by GroupDemoDataService. The URL parameter is brm_group_demo, with modes owner, leader, member, full, and no_seats. Demo mode is ignored unless the current user has RoleCapabilityRegistry::CAP_MANAGE_GROUPS; non-admin frontend users must never receive demo group context.

Custom Dynamic Tags

add_filter( 'bricks/dynamic_tags_list', function( $tags ) {
    $tags[] = array(
        'name'  => '{brm_group:custom_field}',
        'label' => 'Group Custom Field',
        'group' => 'BricksMembers',
    );
    return $tags;
} );

add_filter( 'bricks/dynamic_data/render_tag', function( $value, $tag, $post, $context ) {
    if ( strpos( $tag, '{brm_group:custom_field}' ) === false ) {
        return $value;
    }

    $group_id = \BaselMedia\BricksMembers\Modules\Groups\GroupContextReader::get_instance()
        ->get_user_active_group_for_post( get_current_user_id(), get_the_ID() );
    if ( ! $group_id ) {
        return '';
    }

    // Return your custom field value
    return get_post_meta( $group_id, '_custom_group_field', true );
}, 10, 4 );

Database Schema

The Groups module uses four custom tables:

brm_groups

  • id – Primary key
  • name – Group name
  • owner_user_id – WordPress user ID of owner
  • group_type – Type (cohort, team, etc.)
  • total_seats, used_seats – Seat tracking
  • reserved_seats – Pending email invite seat reservations
  • grantable_levels, default_levels – JSON arrays
  • invite_guest_redirect_page_id, invite_post_claim_redirect_page_id – Optional redirect overrides
  • status – active, archived
  • starts_at, expires_at – Dates

brm_group_members

  • id – Primary key
  • group_id – FK to brm_groups
  • user_id – WordPress user ID
  • role – owner, leader, member, or a custom group role slug
  • granted_levels – JSON array of level IDs
  • status – active, removed, transferred
  • joined_at, last_active_at – Timestamps

brm_seat_rules

  • id – Primary key
  • product_ref – Product identifier
  • provider – webhook, surecart, etc.
  • group_type, total_seats – Group settings
  • levels_to_grant – JSON array
  • create_mode – per_purchase, shared

brm_group_invitations

  • id – Primary key
  • group_id – FK to brm_groups
  • token – Unique claim token
  • type – email, link
  • email – Recipient email (for email type)
  • role – Role to assign
  • max_uses, use_count – Usage limits
  • status – pending, claimed, active, exhausted, expired, revoked

Best Practices

  1. Always check module status before using Groups APIs
  2. Use helper functions for simple lookups, services for complex operations
  3. Handle WP_Error returns from service methods
  4. Use filters to customize behavior rather than overriding
  5. Clear caches after direct database operations (use MutationContract)
Get BricksMembers

Start Building Your Membership Site Today

Create, sell, and manage your content without limits. BricksMembers gives you everything you need to build membership and LMS sites with Bricks Builder.

Lifetime updates & bug fixes • Premium support • 0% transaction fees • 60-day money-back guarantee