The LifterLMS REST API and Webhooks: A Practical Introduction
Create an API key, make your first authenticated request to /wp-json/llms/v1, and set up a signed webhook. Then choose between webhooks, polling and…
Read articleLifterLMS development is WordPress development with a few extra rules. The plugin has its own data model for courses, sales and student progress, its own extension points, and some behavior that only shows up once real students and real payments are involved.
This guide explains how that work should be done. If you own the business, it tells you what your developer should be doing and why. If you write the code, it is the map to have open before you start.
Hook, function and post type names below are written exactly as they appear in the LifterLMS source at the time of writing.
LifterLMS keeps its content in WordPress custom post types and most student activity in its own database tables. Knowing which is which decides how you query, export and migrate.
| What it is | Where it lives |
|---|---|
| Courses, sections, lessons | Post types course, section, lesson |
| Quizzes and questions | llms_quiz, llms_question |
| Memberships | llms_membership |
| Access plans (the price and terms of a course or membership) | llms_access_plan |
| Orders and their payments | llms_order, llms_transaction |
| Engagements (automated emails, achievements, certificates) | llms_engagement plus llms_email, llms_achievement, llms_certificate |
| Enrollments and progress | Table {prefix}lifterlms_user_postmeta |
| Quiz attempts | Table {prefix}lifterlms_quiz_attempts |
Two details catch people out. A section is its own post, so a course outline is three post types deep. And enrollment is not a user role or a row in wp_usermeta. It is a status record (enrolled, expired or cancelled) in the LifterLMS table, keyed by the user and the course or membership.
An access plan belongs to one course or membership. When a student checks out, LifterLMS creates an order, records each payment as a transaction, and enrolls the student. Recurring plans keep adding transactions to the same order. Our guide to memberships and access plans covers the business side of this.
Order statuses are post statuses with an llms- prefix, such as llms-pending, llms-active, llms-completed, llms-on-hold and llms-failed. They are not interchangeable. An order that never heard back from the payment gateway stays pending; it does not become failed. Reports or automations that treat the two as one will be wrong.
For student data, use the PHP functions rather than SQL. llms_enroll_student(), llms_unenroll_student() and llms_is_user_enrolled() wrap the LLMS_Student class, and enrolling through them fires the actions that engagements, webhooks and add-ons listen for. Writing rows directly skips all of that.
Hooks are the main way to change behavior, and the LifterLMS code reference lists each one with its parameters and source file. New hooks use the llms_ prefix. Older ones keep lifterlms_ for backward compatibility, so expect to see both.
add_action( 'llms_user_enrolled_in_course', 'acme_log_enrollment', 10, 2 );
function acme_log_enrollment( $user_id, $course_id ) {
llms_log(
sprintf( 'User %d enrolled in course %d', $user_id, $course_id ),
'acme-enrollments'
);
}
That writes to its own log file, which you can read under LifterLMS > Status > Logs. Swap the function body for a CRM call and you have the pattern behind many integrations.
Front-end markup comes from PHP files in the plugin's templates/ folder. Copy one into a lifterlms/ folder in your child theme, keeping the same sub-path, and LifterLMS loads your copy instead. The details are in how to customize LifterLMS without breaking updates.
The REST API ships with LifterLMS core under /wp-json/llms/v1/. It covers courses, sections, lessons, memberships, access plans, students, enrollments and progress, and recent releases added quizzes, quiz attempts, orders and certificates. Requests authenticate with API keys created under LifterLMS > Settings > REST API, and key authentication only works over HTTPS.
Webhooks are managed on the same screen. Each one posts JSON to your URL when something happens, for example enrollment.created or student.created, and signs the delivery with an X-LLMS-Webhook-Signature header so the receiver can verify it. By default deliveries are queued in the background instead of being sent during the student's page load.
The wp llms commands manage courses, students, enrollments and progress from the shell, which helps with scripted setup and migrations. For a settings screen of your own, extend LLMS_Abstract_Integration and register the class through the lifterlms_integrations filter. Payment gateways extend LLMS_Payment_Gateway and register through lifterlms_payment_gateways. If the goal is connecting a CRM or email tool, check the existing LifterLMS integrations first.
The rule that keeps you out of trouble: code moves up, data moves down. Production holds orders, enrollments and progress that change by the minute, so a staging database never gets pushed over it. Custom code goes up through version control. Course content built on staging can travel with the Export bulk action on the Courses list and LifterLMS > Import.
LifterLMS helps with one staging risk. When a copy's URL no longer matches the address the original site stored, LifterLMS treats it as a clone and stops automatic recurring payments there. Until recently that only took effect once an administrator had opened the dashboard on the copy, so do not rely on detection alone: define LLMS_SITE_IS_CLONE as true in the staging wp-config.php. Recurring payments are the only feature this protects. Engagement emails and webhooks still fire, so put gateways in test mode, route staging mail to a catcher, and disable or re-point webhooks.
LMS bugs are rarely visual. They are a student who paid and cannot get in, or one who did not pay and can. Test the paths that carry money and access:
Automate what you can. LifterLMS core has PHPUnit and Playwright end-to-end test suites, and custom plugins deserve the same for their own logic. For manual checks on staging, the built-in Manual payment gateway creates orders without a card, so you can walk an order through its statuses by hand.
Upgrade-safe code survives LifterLMS and WordPress updates without anyone re-applying changes by hand. In practice:
If you are planning a build, write down the data you need to store and the events you need to react to, then map each one to a post type, table, hook or endpoint from this guide. Whatever does not map cleanly is where the real design work is, and it may be a sign that you need a custom LifterLMS plugin.
Small changes are within reach of a careful in-house developer. Bring in specialists when the work touches checkout, access rules, recurring billing or a large migration, where mistakes cost revenue or student trust. That is what our LifterLMS development service is for, LifterLMS plugin development covers standalone extensions, and you can tell us about your project whenever you are ready.
Create an API key, make your first authenticated request to /wp-json/llms/v1, and set up a signed webhook. Then choose between webhooks, polling and…
Read article
Before you commission a custom LifterLMS plugin, rule out a setting, an add-on and a snippet. Here are the signs you have outgrown them and what a…
Read article
Build one LifterLMS course from start to finish: the course post, Course Options, the outline, lesson content, a quiz, an access plan, publishing…
Read articleSkip the trial and error. Get specialist LifterLMS help and ship faster.