To get a user's course progress percentage in LearnDash, call learndash_course_progress() with 'array' => true. It returns an array with percentage, completed and total, and it is the same calculation the LearnDash progress bar uses. When your code runs without a logged-in user (cron, WP-CLI, a webhook handler), read the raw numbers with learndash_user_get_course_progress( $user_id, $course_id, 'summary' ) and divide them yourself, because the first function deliberately returns zeros when the current user is not allowed to see the target user's data.
This guide answers the "LearnDash get course progress percentage" question with three PHP methods, the REST API route for headless and external systems, how LearnDash counts a "step", and why the number on screen sometimes surprises people. It is written for WordPress developers building dashboards, reports and integrations on LearnDash.
Checked against the LearnDash 4.18.0.1 source (includes/shortcodes/ld_course_progress.php, includes/classes/class-ldlms-model-user-course-progress.php, includes/course/ld-course-steps-functions.php and src/Core/Models/Course.php). LearnDash 5.x is the current release line (5.2.3 as of late October 2026, per the official changelog), and these functions are long-standing, but confirm them on your version before you ship.
How LearnDash counts course progress
Every method below divides completed steps by total steps, so it pays to know what a step is. In 4.18.0.1, LDLMS_Course_Steps::get_steps_count() counts:
- every lesson in the course
- every topic in the course
- plus one step if the course has any course-level (final) quizzes, however many there are
Quizzes attached to a lesson or topic are not counted as steps of their own. They block the parent lesson or topic from being completed, so they affect progress indirectly. The course-level quiz step only counts as complete when all course-level quizzes are complete.
Two more rules shape the final number:
- If the user has a
course_completed_{course_id}user meta value, the summary setscompletedequal tototaland the status tocompleted. A completed course always reads 100%, even if steps were added afterwards. - The percentage is truncated, not rounded:
intval( $completed * 100 / $total ). Seven of nine steps shows 77%, not 78%.
Method 1: learndash_course_progress() with array => true
This is the function behind the [learndash_course_progress] shortcode. Pass 'array' => true and you get data instead of HTML:
<?php
// Not run on a live site; test on staging first.
$progress = learndash_course_progress(
array(
'course_id' => 123,
'user_id' => get_current_user_id(),
'array' => true,
)
);
// array( 'percentage' => 77, 'completed' => 7, 'total' => 9 )
echo esc_html( $progress['percentage'] . '%' );
Use it when you want your number to match what the learner sees in the LD30 course header and profile rows, which call the same function. It has one behaviour to know about. Since 4.11.0 it runs the user ID through learndash_shortcode_protect_user():
- If nobody is logged in, the user ID becomes 0 and you get zeros.
- Administrators can read any user.
- Group Leaders can read users in their groups (or anyone, if the Group Leader Manage Users setting is on and set to Advanced).
- Everyone else can only read themselves.
It also checks learndash_shortcode_can_current_user_access_post() for the course. That protection is right for a shortcode, but it means this function is the wrong tool in a cron job, a CLI command or a REST callback that runs as no user.
Method 2: the summary progress array
learndash_user_get_course_progress( $user_id, $course_id, $type ) (since 3.4.0) returns the progress model without any current-user checks. With $type = 'summary' it returns completed, total and status (not_started, in_progress or completed). Wrap it in your own helper:
<?php
// Not run on a live site; test on staging first.
function aio_ld_course_progress_percentage( $user_id, $course_id ) {
$user_id = absint( $user_id );
$course_id = absint( $course_id );
if ( ! $user_id || ! $course_id || ! function_exists( 'learndash_user_get_course_progress' ) ) {
return 0;
}
$summary = learndash_user_get_course_progress( $user_id, $course_id, 'summary' );
$total = isset( $summary['total'] ) ? absint( $summary['total'] ) : 0;
$completed = isset( $summary['completed'] ) ? absint( $summary['completed'] ) : 0;
if ( 0 === $total ) {
return 0;
}
return min( 100, (int) ( $completed * 100 / $total ) );
}
Because this skips the permission checks, the permission check is now your job. Before you show another user's percentage, confirm the viewer may see it, for example with current_user_can( 'edit_users' ) or learndash_is_group_leader_of_user( get_current_user_id(), $user_id ). The summary is cached per request by LDLMS_Factory_User::course_progress(), so calling it several times for the same user on one page is cheap.
Method 3: the Course model (4.6.0 and later)
The newer model layer has a method that does the same division:
<?php
// Not run on a live site; test on staging first.
use LearnDash\Core\Models\Course;
$course = Course::find( 123 );
$user = wp_get_current_user();
if ( $course && $user->exists() ) {
$percent = $course->get_progress_percentage( $user ); // int 0-100
}
get_progress_percentage( WP_User $user ) divides get_completed_steps_number() by get_total_steps_number(), caps the result at 100 and memoizes it. It counts completed steps from the stored progress tree rather than the summary, so it does not apply the "completed course is always 100%" rule in the same way. The source marks its filter, learndash_model_course_progress_percentage, as @ignore and the method itself carries a "maybe this method is not needed" note, so treat the model API as less stable than the two functions above.
Showing the percentage with a shortcode
If you only need to display progress, no PHP is required. The shortcode accepts course_id and user_id:
[learndash_course_progress course_id="123"]
Without a course_id it uses the course of the current page, and without a user_id it uses the logged-in user. It renders the course_progress_widget template with the "X out of Y steps completed" message. For a full learner page built around these numbers, see our guide to a custom student dashboard in WordPress.
Getting progress over the REST API
For a mobile app, a headless front end or an external system, LearnDash REST API v2 exposes the same summary:
GET /wp-json/ldlms/v2/users/{user_id}/course-progress/{course_id}
The response includes steps_completed, steps_total, progress_status (not-started, in-progress or completed), last_step, date_started and date_completed. There is no percentage field, so compute it on the client with the same truncation if you want it to match the site. GET /wp-json/ldlms/v2/users/{user_id}/course-progress lists every course for that user.
The permission check allows administrators, the user themselves and that user's Group Leaders. Authenticate with an Application Password for a service account rather than a real admin login. The users and course-progress path segments are the defaults and can be renamed in the LearnDash REST API settings, so check them on your site.
Progress for many users: reports and exports
Calling the helper above in a loop is fine for a Group Leader screen with a few dozen learners. For thousands of users it gets slow, because each user's progress model loads their _sfwd-course_progress user meta and each course loads its step tree. Practical options:
- Run large reports in WP-CLI or a background job, in batches, and store the result in your own table.
- Use a persistent object cache. LearnDash keeps its cached data in the WordPress object cache, which otherwise lasts only for one request.
- Use group-level numbers where they fit:
learndash_get_user_group_progress( $group_id, $user_id )returns apercentagefor a group, but it is the share of the group's courses completed (rounded up withceil()), not steps.
Our article on custom LMS reports in WordPress covers the batch and storage patterns in more detail.
Why the percentage looks wrong
| Symptom | Likely cause |
|---|---|
| Shows 0% in cron, CLI or REST code | learndash_course_progress() returned zeros because no user is logged in. Use the summary array. |
| 100% while new lessons are unfinished | The course was completed before the lessons were added; completion forces 100%. |
| Stuck below 100% with all lessons done | A course-level quiz is not passed, or a lesson quiz is blocking a lesson. |
| Different from your own calculation | You counted quizzes as separate steps, or rounded instead of truncating. |
| Wrong course on shared steps | Pass the course ID explicitly; see learndash_get_course_id() for how the course is resolved. |
For the wider data model behind progress, access and completion, start with our LearnDash guide.
How we can help
If you need progress data in a CRM, an HR system or a reporting warehouse, or a dashboard that stays fast with thousands of learners, our LMS plugin development team builds it as a small, upgrade-safe plugin.
Frequently asked questions
How do I get the course progress percentage in LearnDash with PHP?
Call learndash_course_progress( array( 'course_id' => $course_id, 'user_id' => $user_id, 'array' => true ) ). It returns an array with percentage, completed and total. Outside a logged-in request, use learndash_user_get_course_progress( $user_id, $course_id, 'summary' ) and divide completed by total.
Why does learndash_course_progress() return 0 for every user?
Since 4.11.0 the function checks the current user. When nobody is logged in, or the current user is not an administrator, the target user or their Group Leader, the user ID becomes 0 and the result is all zeros. Cron jobs, WP-CLI commands and anonymous REST callbacks hit this.
Do quizzes count towards LearnDash course progress?
Only course-level quizzes, and all of them together count as one step. Quizzes inside a lesson or topic are not separate steps, but they stop the parent lesson or topic from being completed, so they still hold progress back.
Does the LearnDash REST API return a progress percentage?
No. GET /wp-json/ldlms/v2/users/{id}/course-progress/{course} returns steps_completed, steps_total and progress_status. Calculate the percentage on the client and truncate it to match what LearnDash shows.
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 shortcodes and blocks - LearnDash documentation
- LearnDash LMS changelog - LearnDash documentation
- Application Passwords - WordPress.org
Learn more about this topic with AI
Gemini opens with the prompt copied - paste it into the chat to start.