To check if a user has access to a LearnDash course in PHP, call sfwd_lms_has_access( $course_id, $user_id ). It returns true or false, and it is the same check LearnDash runs before it shows course content, so it already accounts for the access mode, direct enrollment, group enrollment, course start and end dates and access expiry. The older ld_course_check_user_access() is a thin wrapper that calls it, and for code that works with the newer models there is Product::user_has_access(), which handles groups as well as courses.
This guide shows the call, explains what LearnDash actually checks behind it, and covers the edge cases that make a learndash check user has access course test return a surprising answer: administrators, open courses, lesson IDs and expired access. It is written for WordPress developers adding gated content, custom dashboards or integrations to a LearnDash site.
Checked against the LearnDash 4.18.0.1 source code. LearnDash 5.x is the current major release (5.2.3 as of late October 2026, per the official changelog), so confirm the function bodies against the version you run before you rely on a detail below.
The short answer: sfwd_lms_has_access()
The function lives in includes/course/ld-course-user-functions.php and takes a post ID and an optional user ID. If you leave the user ID out, it uses the current user.
<?php
// Not run on a live site; test on staging first.
$aio_course_id = 123;
$aio_user_id = get_current_user_id();
if ( $aio_user_id && sfwd_lms_has_access( $aio_course_id, $aio_user_id ) ) {
// The user can open this course.
}
Pass the user ID explicitly whenever you are not checking the current visitor, for example in cron jobs, REST callbacks or admin reports. The $aio_user_id && test matters for logged-out visitors: as the next section shows, some courses return true for user ID 0.
The result passes through the sfwd_lms_has_access filter (arguments: the result, the post ID and the user ID), so another plugin or your own code can change it. That is useful, and it also means the function is the right place to ask the question: if you read user meta yourself, you skip every filter that the rest of the site respects.
What LearnDash checks, in order
In 4.18.0.1, sfwd_lms_has_access_fn() does the work and returns at the first rule that decides the answer:
- Resolve the course. It calls
learndash_get_course_id()on the post ID. If the post does not belong to a course, it returnstrue. - Admin and Group Leader auto-enrollment. If
learndash_can_user_autoenroll_courses()is true for the user, access is granted. That depends on the Course Auto-enrollment switches in the Admin User Settings and Group Leader User Settings sections. - Sample lessons. If the post is a lesson marked as a sample, access is granted.
- Access mode. An Open course returns
true. A Buy Now course with no price set also returnstrue. - No user. If there is still no user ID at this point, it returns
false. - Enrollment. The user needs either the
course_{ID}_access_fromuser meta (direct enrollment) or enrollment through a group, checked withlearndash_user_group_enrolled_to_course(). Sites still on the legacy course access list check that list instead. - Dates. For direct enrollment, the course must have started and not ended, using the Product model's
has_started()andhas_ended(). Group enrollment skips this date check. - Expiry. Finally,
ld_course_access_expired()decides whether the course's access period has run out for this user.
The access modes themselves (Open, Free, Buy Now, Recurring and Closed) are explained in our guide to the LearnDash course access mode.
Access, enrollment and progress are different questions
A common bug is answering one of these with a function built for another. Use the one that matches the question:
| Question | Function | Returns |
|---|---|---|
| Can this user open the course right now? | sfwd_lms_has_access( $course_id, $user_id ) | bool |
| Can this user open this course or group product? | Product::find( $id )->user_has_access( $user_id ) | bool |
| Which courses can the user see? | learndash_user_get_enrolled_courses( $user_id ) | array of course IDs |
| Is the access coming through a group? | learndash_user_group_enrolled_to_course( $user_id, $course_id ) | bool |
| When was the user given direct access? | ld_course_access_from( $course_id, $user_id ) | timestamp, or false |
| Has the user started or finished? | learndash_course_status( $course_id, $user_id, true ) | status slug |
Note that learndash_user_get_enrolled_courses() adds every Open course to the list, because anyone can open those. If you want "courses this user was actually enrolled in", filter Open courses out of the result.
Step by step: gate a page or resource on course access
A typical job is protecting something that is not a LearnDash post, such as a workbook page or a download, so that only people with access to a course can open it. The function below redirects everyone else to the course page. It is not run on a live site; test on staging first.
<?php
/**
* Redirect visitors without access to course 123 away from the workbook page.
* Not run on a live site; test on staging first.
*/
function aio_ld_guard_workbook_page() {
if ( ! is_page( 'course-workbook' ) ) {
return;
}
$course_id = 123;
$user_id = get_current_user_id();
if ( $user_id && sfwd_lms_has_access( $course_id, $user_id ) ) {
return;
}
wp_safe_redirect( get_permalink( $course_id ) );
exit;
}
add_action( 'template_redirect', 'aio_ld_guard_workbook_page' );
- Replace
course-workbookwith your page slug and123with the course ID. - Put the function in a small site plugin rather than the theme, so a theme switch does not remove the protection.
- Test with four accounts: an enrolled student, a student enrolled only through a group, a user with no access and an administrator.
- Check the course page after a redirect. If the user has no access, they should see the enrollment button for that course.
For content inside a post rather than a whole page, you may not need code at all. The [student] shortcode shows its content only to logged-in users with access to the course given in its course_id attribute (or the group in group_id), and [visitor] does the opposite. If you want to control where LearnDash itself sends users without access, see redirecting users without access in LearnDash.
Checking access to a group or through the Product model
Since LearnDash 4.5.0, courses and groups can both be handled as products through LearnDash\Core\Models\Product. Its user_has_access() method checks the product's start and end dates, then calls sfwd_lms_has_access() for a course or learndash_is_user_in_group() for a group, and passes the result through the learndash_model_product_user_has_access filter.
<?php
// Not run on a live site; test on staging first.
use LearnDash\Core\Models\Product;
$aio_product = Product::find( absint( $aio_group_id ) );
if ( $aio_product && $aio_product->user_has_access( $aio_user_id ) ) {
// Member of the group, inside its start and end dates.
}
Use it when one code path must handle both courses and groups. It returns false for user ID 0, which avoids the logged-out surprise described below. To put users into a group from code in the first place, see adding a user to a LearnDash group programmatically.
Edge cases that return a surprising answer
- Administrators get true for every course when Course Auto-enrollment is on for admin users, which is the setting's initial value. Test as a real student, never as an admin.
- Group Leaders can too. The same switch exists in the Group Leader User Settings. When it is on, Group Leaders pass the check for all courses.
- Open courses return true for logged-out visitors. The function checks the access mode before it checks for a user, so add your own
is_user_logged_in()or user ID test when a login is required. - A post outside any course returns true. Pass an ID that is not a course or a course step, or a lesson that has not been attached to a course yet, and the answer is
true. Validate the ID first, for example withget_post_type(). - Lesson IDs follow the course. Passing a lesson, topic or quiz ID checks access to its course, not completion of earlier steps. Progression and prerequisites are separate checks.
- The check can revoke expired access. When the access period has run out,
ld_course_access_expired()removes the user's course access, fireslearndash_user_course_access_expiredand, if the course is set to delete progress on expiry, deletes it. Your check is not read-only for expired users. To give learners more time, see extending LearnDash course access. - Group access ignores course start and end dates. Users enrolled through a group skip the course date check in 4.18.0.1, although expiry still applies.
Changing the answer with the sfwd_lms_has_access filter
If access depends on something LearnDash does not know about, such as an external subscription or an HR system, filter the result rather than replacing the check:
<?php
// Not run on a live site; test on staging first.
function aio_ld_external_access( $has_access, $post_id, $user_id ) {
if ( $has_access || empty( $user_id ) ) {
return $has_access;
}
// aio_external_entitlement() is your own function.
return (bool) aio_external_entitlement( absint( $user_id ), learndash_get_course_id( $post_id ) );
}
add_filter( 'sfwd_lms_has_access', 'aio_ld_external_access', 10, 3 );
Keep the callback fast and cache the external answer, because LearnDash runs this check many times per page on course and lesson screens. Remember that a filter changes what this check reports, but it does not enroll the user, so they will not appear in enrollment reports or receive course progress until they are enrolled.
How we can help
We build LearnDash access rules, gated resources and integrations that keep enrollment in step with your CRM, store or HR system. If you need custom access logic that stays correct across LearnDash updates, see our LMS plugin development service, or start with our LearnDash guide for the wider data model.
Frequently asked questions
How do I check if a user is enrolled in a LearnDash course?
Call sfwd_lms_has_access( $course_id, $user_id ), which returns true when the user can open the course. To list every course a user can see, use learndash_user_get_enrolled_courses( $user_id ), and remember that it also includes all Open courses.
What is the difference between sfwd_lms_has_access and ld_course_check_user_access?
None in practice. In LearnDash 4.18.0.1, ld_course_check_user_access() is a one-line wrapper that returns sfwd_lms_has_access(). New code can call sfwd_lms_has_access() directly.
Why does sfwd_lms_has_access return true for an administrator who is not enrolled?
The Course Auto-enrollment setting in the Admin User Settings gives administrators access to every course without enrollment. The same switch exists for Group Leaders. Turn it off, or test with a student account.
Can I check LearnDash group access in PHP?
Yes. Use learndash_is_user_in_group( $user_id, $group_id ) for membership, or Product::find( $group_id )->user_has_access( $user_id ) if you also want the group's start and end dates applied.
Free 30-minute consultation
Not sure where to start? Talk to an LMS engineer.
Tell us what platform you run and what you need. You’ll get honest, practical advice - even if the answer is that you don’t need us.
Sources & references
- LearnDash developer documentation - LearnDash
- learndash_access_redirect hook - LearnDash
- LearnDash LMS changelog - LearnDash documentation
Learn more about this topic with AI
Gemini opens with the prompt copied - paste it into the chat to start.