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 a custom plugin for each integration.

  • Add as a preferred source on Google
Illustration for “The LifterLMS REST API and Webhooks: A Practical Introduction”

Sooner 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.

Create an API key

Keys are managed under LifterLMS > Settings > REST API, in the API Keys section. Administrators and LMS Managers can reach it.

  1. Click Add API Key.
  2. Enter a Description that names the system using the key, such as "CRM sync".
  3. Choose the User. The key acts as that WordPress user, so their role and capabilities decide what it may touch.
  4. Choose Permissions: Read, Write, or Read / Write.
  5. Save, then copy the consumer key and consumer secret, or use Download Keys.

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.

Authenticate and make a request

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.

The main resources

ResourceRoute 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.

Set up a webhook

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.

Topics

A topic is a resource and an event joined by a dot. The main ones:

  • Content: course, section, lesson, membership and access_plan, each with created, updated and deleted.
  • People: student.created, student.updated, student.deleted, and the same three for instructor.
  • Learning: enrollment.created, enrollment.updated, enrollment.deleted, progress.updated, quiz-attempt.completed, quiz-attempt.graded, awarded-certificate.created.
  • Sales: order.created, order.updated, transaction.created, transaction.updated.
  • Action: a generic topic that fires on any WordPress action hook you name.

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.

What arrives

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.

Verify the signature

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.

Delivery, failures and logs

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:

  • A failed delivery is not sent again. LifterLMS counts the failure and moves on. After more than five failures in a row it sets the webhook to Disabled. A successful delivery resets the count.
  • Redirects are not followed. A 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.
  • Delivery logging is off by default. Define 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.

Zapier and other automation tools

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.

Webhooks, polling or a custom plugin

ApproachUse it whenWatch for
WebhooksAnother system must react within moments, such as a CRM tag on enrollmentNo retries, so add reconciliation
Polling the APIA nightly or hourly sync is enough, such as completions into a reporting databasePaging through large lists, and load at busy hours
Calling the API on demandThe outside system starts the action, such as enrolling a customer after a saleWrite keys stored outside your site
A custom pluginThe logic belongs inside WordPress: rules across several events, data the API does not expose, or checks that must run before an enrollment is allowedCode 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.

Security notes

  • One key per system. Separate keys can be revoked separately.
  • A dedicated user per key. Tie integration keys to a service account with the lowest role that works, not to a person's administrator login. Deleting a user deletes their keys.
  • Least permission. Read keys for anything that only reports, and no secrets in repositories or shared documents.
  • Verify every delivery. An unverified webhook endpoint that grants access is an open door.
  • Keep staging quiet. A cloned site carries its webhooks with it. Disable or re-point them before testing.
  • Check your firewall. Security plugins that block /wp-json or strip custom headers will break key authentication.

What to do next

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.

Add LifterLMS Expert to your preferred sources

Add lifterlmsexpert as a preferred source on Google, or open this article in your AI assistant to use it as a source.

  • Add as a preferred source on Google
VishavjeetChoubeyLifterLMS Expert
Free consultation

Prefer expert help over DIY?

Skip the trial and error. Get specialist LifterLMS help and ship faster.

Talk to a LifterLMS Expert Explore our services

Replies within one business day