Gating

Gating AI Features by Membership Level

⏱ 15 min

An AI tutor is expensive to run per question, which makes it a natural candidate for a premium membership tier rather than a blanket feature every member gets. This lesson puts MemberPress’s own membership check directly inside the tutor ability’s permission_callback, exactly where an Abilities API permission check belongs, so the AI feature itself becomes a real, enforced tier boundary.

What you'll learn in this lesson
Why permission_callback is the correct place for this check
It runs before execute_callback, on every single call, with no separate code path to forget.
The real distinction between is_active() and is_active_on_membership()
Being an active member of anything versus being active on the specific tier that unlocks this feature.
Layering LearnDash enrollment and MemberPress tier checks together
Both conditions have to pass, neither replaces the other.
Why MemberPress needs its own version-checking discipline
No single canonical SDK reference exists the way LearnDash publishes one.
Prerequisites

MemberPress installed and active, with at least two membership products at different tiers. This lesson builds directly on Lesson 2’s lms-tutor/ask-tutor ability.

Step 1: Confirm MeprUser against your actual installed version first

MemberPress doesn’t publish one single canonical class and method reference the way LearnDash’s developer site does. The methods below, is_active(), is_active_on_membership( MeprProduct $prd ), and active_product_subscriptions(), are real and current as of this writing, but MemberPress ships as a plugin you update independently, and its internal class shape can change between major versions. Check wp-content/plugins/memberpress/app/models/MeprUser.php in your actual installed version before shipping this, rather than trusting this lesson’s signatures blindly on a version you haven’t looked at.

Step 2: The distinction that actually matters: is_active() vs is_active_on_membership()

// File: lms-tutor/class-tutor-gating.php
$mepr_user = new MeprUser( get_current_user_id() );

// Wrong for tiered gating: true for ANY active membership, regardless of which one.
if ( $mepr_user->is_active() ) { /* ... */ }

// Correct for tiered gating: true only for this specific membership product.
$premium_tutor_product = new MeprProduct( PREMIUM_TUTOR_MEMBERSHIP_ID );
if ( $mepr_user->is_active_on_membership( $premium_tutor_product ) ) { /* ... */ }

is_active() answers “is this account a paying member of anything at all.” A site with a $9 basic tier and a $49 premium tier that gates the tutor on is_active() alone would let every paying member in, including the $9 tier the tutor was never meant to reach. is_active_on_membership() checks against one specific MeprProduct, which is the actual tier boundary you’re trying to enforce.

Step 3: Wire it into the tutor’s permission_callback

// File: lms-tutor/class-tutor-gating.php
define( 'PREMIUM_TUTOR_MEMBERSHIP_ID', 42 ); // Your actual MemberPress membership product ID.

add_action( 'wp_abilities_api_init', function () {
	wp_register_ability(
		'lms-tutor/ask-tutor',
		array(
			'label'               => __( 'Ask the course tutor', 'lms-tutor' ),
			'input_schema'        => array(
				'type'       => 'object',
				'properties' => array(
					'lesson_id' => array( 'type' => 'integer' ),
					'question'  => array( 'type' => 'string' ),
				),
				'required'   => array( 'lesson_id', 'question' ),
			),
			'permission_callback' => function ( $input ) {
				// LearnDash: does this account have access to this course resource at all?
				if ( ! sfwd_lms_has_access( $input['lesson_id'], get_current_user_id() ) ) {
					return new WP_Error( 'not_enrolled', 'You are not enrolled in this course.', array( 'status' => 403 ) );
				}

				// MemberPress: is this account on the specific tier that unlocks the AI tutor?
				$mepr_user = new MeprUser( get_current_user_id() );
				$product   = new MeprProduct( PREMIUM_TUTOR_MEMBERSHIP_ID );

				if ( ! $mepr_user->is_active_on_membership( $product ) ) {
					return new WP_Error( 'not_premium', 'The AI tutor is a premium feature. Upgrade to unlock it.', array( 'status' => 403 ) );
				}

				return true;
			},
			'execute_callback'    => 'lms_tutor_ask',
		)
	);
} );

Both checks have to pass. Enrollment answers “does this account belong in this course.” Membership tier answers “does this account’s plan include the AI tutor specifically.” Neither one substitutes for the other, and a WP_Error with a specific reason lets your front end show “upgrade to unlock” instead of a generic denial.

Step 4: A softer tier: limited questions instead of no access

A middle tier might get a capped number of tutor questions per day rather than none at all, using active_product_subscriptions() to check which tier applies and a transient as a simple daily counter:

// File: lms-tutor/class-tutor-gating.php
function lms_tutor_check_daily_limit( int $user_id, int $limit ): bool {
	$key   = "lms_tutor_daily_count_{$user_id}_" . gmdate( 'Y-m-d' );
	$count = (int) get_transient( $key );

	if ( $count >= $limit ) {
		return false;
	}

	set_transient( $key, $count + 1, DAY_IN_SECONDS );
	return true;
}

Called inside the same permission_callback, a mid-tier member gets, say, five questions a day before hitting a clear limit, while the premium tier’s is_active_on_membership() check bypasses the counter entirely.

Test it: confirm both denial paths, not just the happy path

wp-env run cli wp eval '
// Test user active on the $9 tier, not the $49 premium tutor tier.
wp_set_current_user( 9 );
print_r( wp_get_ability( "lms-tutor/ask-tutor" )->execute( array( "lesson_id" => 12, "question" => "Explain lesson 3." ) ) );
'

Confirm this returns the not_premium error, not a generated answer, then repeat with a test user actually active on the premium product ID and confirm the ability executes normally.

is_active() alone silently unlocks every paying tier

The single most common mistake gating an AI feature by membership is reaching for is_active() because it reads as “are they a member,” gets the check working against your own premium test account, and ships it, only to discover every tier, including the cheapest one, now has the expensive AI feature too. Always gate on is_active_on_membership() against the specific product ID the feature is meant to belong to, and test with a low-tier account specifically, not just your own admin or premium test user.

Recap

permission_callback is exactly where a membership check belongs: it runs on every call, before execute_callback, with no separate code path to accidentally skip. is_active_on_membership( MeprProduct $prd ), not is_active(), is the real check for tiered gating, since is_active() alone treats every paying tier as equivalent. Combine it with LearnDash’s own sfwd_lms_has_access() enrollment check rather than replacing it, and confirm MemberPress’s exact class shape against your actual installed version before relying on it, since no single canonical reference covers every release.

Resources & further reading

← Auto-Grading Free-Text Answers and Giving Feedback Integrating Deeply With LearnDash's Content Structure →