LifterLMS Development: Everything You Need to Know
LifterLMS development is WordPress development with extra rules: content lives in post types, student activity in custom tables, changes go through…
Read articleSooner or later another system needs to know what happens inside your LMS. The CRM wants new students, the HR platform wants completions, and a sales tool wants to enroll a customer when a deal closes.
LifterLMS has two tools for this in the free core plugin. The REST API lets outside software read and change LMS data on request. Webhooks push a message out when something happens. No extra plugin is needed for either.
This introduction takes you from an API key to a first request and a verified webhook. Routes, headers and labels were checked against the plugin source at the time of writing.
Keys are managed under LifterLMS > Settings > REST API, in the API Keys section. Administrators and LMS Managers can reach it.
Do the copying before you leave the page. The consumer key is stored as a hash, and LifterLMS shows the pair once. Afterward the screen shows only the last characters of the key and a "Last accessed at" time. A key is withdrawn with Revoke.
The permission setting works by HTTP method. Read allows GET and HEAD. Write allows POST, PUT, PATCH and DELETE.
All routes sit under /wp-json/llms/v1. Send the key pair in one of two ways: as the headers X-LLMS-CONSUMER-KEY and X-LLMS-CONSUMER-SECRET, or as HTTP Basic authentication with the key as the username and the secret as the password.
Key authentication only runs over HTTPS. On a plain HTTP request LifterLMS ignores the credentials, and the call is treated as anonymous.
List the first five courses:
curl "https://example.com/wp-json/llms/v1/courses?per_page=5" \
-H "X-LLMS-CONSUMER-KEY: ck_your_key" \
-H "X-LLMS-CONSUMER-SECRET: cs_your_secret"
Enroll student 123 in course 456, using Basic authentication and a key with write permission:
curl -X POST \
"https://example.com/wp-json/llms/v1/students/123/enrollments/456" \
-u "ck_your_key:cs_your_secret"
A successful enrollment returns status 201 and a JSON object with student_id, post_id, status and the dates. If the student is already enrolled the API answers with a bad request error and tells you to use PATCH on the same route to change the status instead. The same route accepts a membership ID in place of the course ID.
If Basic authentication fails on a host where the header version works, the server is probably not passing the Authorization header through to PHP. Use the two custom headers.
Collections are paged with page and per_page, and the response headers X-WP-Total and X-WP-TotalPages tell you how much there is. Enrolling through the API runs the same code as a normal enrollment, so engagements, notifications and webhooks fire as usual.
| Resource | Route under /wp-json/llms/v1 |
|---|---|
| Courses, sections, lessons | /courses, /sections, /lessons |
| Memberships and access plans | /memberships, /access-plans |
| Students and instructors | /students, /instructors |
| A student's enrollments | /students/{id}/enrollments |
| A student's progress in a course or lesson | /students/{id}/progress/{post_id} |
| A student's grades (read only) | /students/{id}/grades |
| Everyone enrolled in a course | /courses/{id}/enrollments |
| Orders and their transactions | /orders, /orders/{id}/transactions |
| Quizzes, questions and attempts | /quizzes, /questions, /quiz-attempts |
| Certificate templates and awarded certificates | /certificates, /awarded-certificates |
| Webhooks and API keys | /webhooks, /api-keys |
The student list takes an enrolled_in filter with one or more course or membership IDs. Quizzes, orders and certificates arrived in recent releases, so check your LifterLMS version if those routes return not found. The REST API reference lists every field.
Webhooks are in the Webhooks section of the same settings tab. Click Add Webhook and fill in five fields: Name, Status, Topic, Delivery URL and Secret Key. Set the status to Active when you are ready for it to send.
When you save a new or changed Delivery URL, LifterLMS sends a test request to it and expects a 200 response. If the receiver answers with anything else the webhook is not saved, so have the endpoint running first, at a public address.
A topic is a resource and an event joined by a dot. The main ones:
course, section, lesson, membership and access_plan, each with created, updated and deleted.student.created, student.updated, student.deleted, and the same three for instructor.enrollment.created, enrollment.updated, enrollment.deleted, progress.updated, quiz-attempt.completed, quiz-attempt.graded, awarded-certificate.created.order.created, order.updated, transaction.created, transaction.updated.One detail matters for access sync. When a student is removed from a course or their access expires, the event is enrollment.updated with a new status. enrollment.deleted is only sent when the enrollment record itself is erased.
The body is JSON. For created and updated events it is the same object the REST API returns for that resource, built with the permissions of the user who created the webhook. For deleted events it carries only the IDs. Headers identify the delivery: X-LLMS-Webhook-Topic, X-LLMS-Webhook-ID, X-LLMS-Delivery-ID and X-LLMS-Webhook-Signature, which proves the sender.
The signature header looks like t=1790000000,v1=abc123.... The v1 value is an HMAC SHA-256 hash of the timestamp, a dot and the raw request body, keyed with the webhook's Secret Key. A receiver in PHP checks it like this:
$secret = 'your-webhook-secret-key';
$body = file_get_contents( 'php://input' );
$header = $_SERVER['HTTP_X_LLMS_WEBHOOK_SIGNATURE'] ?? '';
$parts = array();
foreach ( explode( ',', $header ) as $pair ) {
$kv = explode( '=', $pair, 2 );
$parts[ $kv[0] ] = $kv[1] ?? '';
}
$expected = hash_hmac( 'sha256', ( $parts['t'] ?? '' ) . '.' . $body, $secret );
if ( ! hash_equals( $expected, $parts['v1'] ?? '' ) ) {
http_response_code( 401 );
exit;
}
Hash the body exactly as received, before any JSON decoding. The timestamp is taken from the site's local clock, so on a site whose timezone is not UTC it is offset from standard Unix time. Allow for that if you reject old messages.
Webhooks are not sent while the student waits for a page. LifterLMS queues each delivery in Action Scheduler, the background job runner bundled with the plugin, and you can see the queue under LifterLMS > Status > Scheduled Actions. On a site where WP-Cron is not running, deliveries can sit in that queue along with every other scheduled task.
Three behaviors to know first:
301 or 302 from your endpoint is counted as a success and the payload goes nowhere. Enter the final URL, including the right scheme and trailing slash.LLMS_REST_WEBHOOK_DELIVERY_LOGGING as true in wp-config.php and each webhook writes its own log, readable under LifterLMS > Status > Logs. Those logs can contain student data, so switch logging off again when you have finished debugging.Because nothing is retried, pair important webhooks with a nightly job that reads the API and fixes anything the receiver missed.
The LifterLMS app on Zapier connects with your site URL and a consumer key and secret, so the key screen above is all the setup the WordPress side needs. Its triggers include new student, new course enrollment and course access expired, and its actions include enrolling a user in a course or membership. The Zapier setup guide has the full list.
Zapier checks the API on a schedule, so its triggers are not instant, and an event that is reversed before the next check can be missed. Tools that can receive a webhook and call an HTTP API, such as n8n or Make, can use both halves directly. For connectors inside WordPress, see our guide to LifterLMS integrations.
| Approach | Use it when | Watch for |
|---|---|---|
| Webhooks | Another system must react within moments, such as a CRM tag on enrollment | No retries, so add reconciliation |
| Polling the API | A nightly or hourly sync is enough, such as completions into a reporting database | Paging through large lists, and load at busy hours |
| Calling the API on demand | The outside system starts the action, such as enrolling a customer after a sale | Write keys stored outside your site |
| A custom plugin | The logic belongs inside WordPress: rules across several events, data the API does not expose, or checks that must run before an enrollment is allowed | Code you own and must keep tested |
A custom plugin uses the PHP hooks behind the webhooks, described in LifterLMS development. Our article on when to build a custom LifterLMS plugin covers that decision.
/wp-json or strip custom headers will break key authentication.Create a read-only key on a staging site, run the first curl command, then add a webhook for enrollment.created pointed at a test endpoint and enroll a test student.
Get help when an integration decides who has access or moves money, when it needs retries, monitoring and an audit trail, or when the other system has no connector at all. That is what our LifterLMS integrations and LifterLMS plugin development services are for. Tell us which systems need to talk and we will outline the simplest reliable design.
LifterLMS development is WordPress development with extra rules: content lives in post types, student activity in custom tables, changes go through…
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.