Learnomy for Developers: The Hooks, REST API, and Extension Seams Behind 1.4.0
Here is the test we hold our products to at Wbcom Designs: can a developer extend it without us shipping a new version? For a long time WordPress LMS plugins failed that test. You wanted a new question type, a different completion rule, a report the author never imagined, and your only move was a hacky filter or a fork. So when we built Learnomy, the surface a developer touches got as much design attention as the one a student sees.
1.4.0 is the release where we can finally point at proof instead of promises. The whole advanced-math question system in Learnomy Pro, numeric answers with tolerance, symbolic equivalence, calculated questions with randomized variables, was added without changing a single line of core question-handling code. Pro registered three new question types through one filter, dropped in the editors through a JavaScript registry, and the free plugin never knew the difference. If our own Pro team builds on Learnomy like it is a third-party plugin, you can too.
This is the developer’s tour. Less “what it does,” more “what you can build on it.”
The extension model: register, don’t fork
Start with the question-type registry, because it is the clearest example of the pattern the whole plugin follows. Every question type in Learnomy, multiple choice, short answer, matching, ordering, and now the Pro math types, comes through one filter:
add_filter( 'learnomy_question_types', function ( $types ) {
$types['likert'] = array(
'label' => __( 'Likert scale', 'my-plugin' ),
'category' => 'survey',
);
return $types;
} );
That registers the type on the PHP side, authoring, storage, and grading all recognise it. The editor UI comes through a matching JavaScript registry, window.learnomyQuestionTypeEditors, where you attach a renderer keyed to your type. For anything that behaves like a text answer, there is an even shorter path: push your key onto window.learnomyTextAnswerTypes and the existing text-answer machinery handles it. That is literally how Pro added numeric and math input without rebuilding the quiz player.
The same shape repeats everywhere. Lesson content types are a filter, learnomy_lesson_content_types, and adding one is how SCORM and H5P became first-class lesson kinds in Pro rather than bolted-on iframes. The role a new user is provisioned into runs through learnomy_provision_role. The bulk-enrollment ceiling is learnomy_bulk_enroll_max. The incoming-webhook payload passes through learnomy_incoming_webhook_payload before it is trusted. None of these are afterthoughts sprinkled in for a support ticket; they are the seams the product itself is assembled from.
| You want to add… | Seam | Layer |
|---|---|---|
| A question type | learnomy_question_types + window.learnomyQuestionTypeEditors | PHP + JS |
| A lesson content type | learnomy_lesson_content_types | PHP |
| A custom provisioning role | learnomy_provision_role | PHP |
| Enrollment on external payment | learnomy_incoming_webhook_payload + signed webhook | PHP + HTTP |
| A new content standard | Content_Standards_Bridge | Service |
One completion path, five standards
The part of 1.4.0 we are most proud of architecturally is the one you cannot see. Learnomy Pro now speaks SCORM, xAPI, LTI 1.3, QTI, and H5P, and all five report through a single service, Content_Standards_Bridge, rather than each maintaining its own idea of what “completed” means.
This matters if you are extending it. A SCORM package firing a CMI completed, an H5P interaction emitting an xAPI statement, and a grade coming back over LTI’s AGS all funnel into the same progress and reporting pipeline that a native Learnomy quiz uses. One learner record. One completion state. When you add your own integration, you implement against that bridge and inherit the reporting, the certificates, and the analytics for free. It is also why cmi5 is a small addition rather than a rewrite, the seam it plugs into already exists. Standards stop being silos the moment they share a completion path.
REST-first, so headless is not a fight
Everything a human can do in the browser, the API can do too. That was a hard rule during the build, not a nice-to-have, and it is why the mobile app and the web app are just two clients of the same surface.
Learnomy registers around 180 endpoints under the learnomy/v1 namespace, with Pro adding its own under learnomy-pro/v1:
GET /wp-json/learnomy/v1/courses
POST /wp-json/learnomy/v1/auth/token
GET /wp-json/learnomy/v1/courses/{id}/progress
POST /wp-json/learnomy/v1/quizzes/{id}/attempts
Auth is JWT: POST /auth/token with WordPress credentials returns an access and a refresh token, and you pass Authorization: Bearer on every call. Responses use one envelope everywhere, { "data": [...], "meta": { "total": N, "cursor_next": "..." } } for lists, { "data": { ... } } for a single resource, and a structured { "error": { "code", "message", "request_id" } } on failure. Same shape whether the endpoint is Free or Pro, so a client written against one part of the API already knows how to read the rest. Sensitive write endpoints are rate-limited (60/min per user by default, instructors and admins exempt), and the cursor-based pagination means a 50,000-enrollment list does not fall over.
We already leaned on exactly this shape to ship a native iOS app for Jetonomy. The Learnomy surface is built to the same standard, so a dedicated Learnomy app, a headless Next.js front end, or a sync into another system are all client problems, not plugin problems.

AI-native: the Abilities API, not just endpoints
A REST API tells a machine how to call something. It does not tell the machine what exists or whether it is allowed. That gap is why most “AI + LMS” demos are brittle glue code. Learnomy closes it by registering its actions with the WordPress Abilities API.
Thirty-four abilities across six categories, course management, learning and progress, quizzes, revenue and memberships, and more, are published at the standard namespace:
GET /wp-json/wp-abilities/v1/
Each ability ships a typed input schema, a typed output schema, and a permission check. An agent reads the catalog, discovers learnomy/enroll or learnomy/get-course-stats, learns its exact arguments, and calls it, going through the same capability checks a human would. Nothing is hard-coded against your specific site. This is on by default in the free plugin, and it is the difference between an LMS an assistant can read and one an assistant can safely operate. If you are building agent workflows on WordPress, this is the layer to target.
You inherit the permission model, too
The reason we are comfortable exposing 180 endpoints and 34 agent-callable abilities on the same install is that they all ask the same gatekeeper. Learnomy defines 34 capabilities and routes every access decision, REST, ability, CLI, and admin screen alike, through one permission engine, not a scatter of inline current_user_can() guesses.
That has real consequences for anyone building on it. An anonymous request for a draft course is clamped to published content, so you cannot enumerate unpublished IDs through a query string. An instructor sees and edits their own courses and their own students’ data, and nothing that belongs to another instructor. An admin sees the estate. A student is scoped to their own enrollments, progress, and receipts. These are not checks each endpoint re-implements and occasionally forgets, they are one policy the whole surface consults.
So when you add a report, an endpoint, or an ability on top of Learnomy, you do not start from an open door and bolt on security afterward. You ask the same engine “can this user do this?” and inherit the same clamps the core uses. An extension is secure by default because the default is secure, and the manifest tells you exactly which capability each surface requires so you are never guessing which cap to check. Getting authorization right is usually the hardest part of extending an LMS. Here it is mostly already done.
Webhooks in both directions
The new Closed pricing model in 1.4.0 is, under the hood, a developer feature wearing a merchant’s hat. Sell a course anywhere, Gumroad, a marketplace, a CRM checkout, and grant access here through a signed webhook. Incoming requests must carry a valid HMAC signature, because the endpoint grants paid access and an unsigned call is rejected rather than processed. The payload runs through learnomy_incoming_webhook_payload so you can map an external customer shape onto a Learnomy enrollment. Refunds fire a revoke.
Going the other way, Learnomy emits outbound webhooks on lifecycle events, with the cap governed by learnomy_max_webhooks, so a CRM or a data warehouse can subscribe to enrollments, completions, and payments without polling. Two directions, both signed, both filterable.
Scriptable from the command line
If it is worth doing in the admin, it is worth doing in a script. Learnomy ships a full WP-CLI surface:
wp learnomy course list
wp learnomy membership ...
wp learnomy certificate ...
wp learnomy content ...
wp learnomy db ...
wp learnomy repair student-count
wp learnomy seed-model-site
wp learnomy selfcheck
That last pair matters more than it looks. seed-model-site stands up a realistic dataset, courses, lessons, quizzes, enrollments, so you can develop against something that resembles a real academy instead of three test rows. selfcheck is the plugin auditing its own wiring. Migration tooling lives here too, for pulling a library across from another LMS in resumable batches rather than a single fragile import.
Blocks and the Interactivity API
Learnomy ships a dozen Gutenberg blocks, and the quiz player and catalog run on the WordPress Interactivity API, server-rendered first, then hydrated for client-side interaction, with the same directives WordPress core is standardising on. That means the front end is not a black-box React island you have to reverse-engineer; it is standard WordPress markup and state you can reason about, style, and extend with the tools you already use.
The data engine you do not see
None of the above matters if the thing collapses at 2,000 rows. We have watched too many LMS plugins demo beautifully and then melt when an academy actually grows, it is the whole reason we wrote Scale Is the Feature. So the data layer has rules. Reads go through model classes, never raw $wpdb scattered through templates. Every list has a bounded query with LIMIT and OFFSET and a dedicated COUNT(*) companion, so showing a total never means loading ten thousand objects to call count() on them. Every column a query filters, sorts, or joins on has an index behind it. Dashboards that used to run a query per row were rewritten to batch-fetch.
When you build on Learnomy, you inherit that discipline. A report you add on top of the models is bounded and indexed because the layer underneath it already is. Extending a fast plugin keeps it fast; extending a slow one just adds to the pile.
The contract that keeps Free and Pro honest
One more thing, because it is the part that makes all of this safe to build against. Both plugins ship a machine-readable manifest at audit/manifest.json, every REST endpoint, hook, table, block, capability, and CLI command, enumerated. Free and Pro keep paired manifests specifically so a hook Pro consumes and a hook Free fires cannot silently drift apart between releases. Before a version is tagged, a contract audit checks that nothing Pro depends on vanished from Free.
For you as an extender, that manifest is a map. You do not have to grep the source to learn what seams exist; they are listed, typed, and versioned. It is the difference between building on a documented platform and building on someone’s private internals and hoping.
Where to start
If you want to build on Learnomy 1.4.0, three entry points cover most of what you will need. The learnomy/v1 REST namespace if you are writing a client or a headless front end. The filter seams, learnomy_question_types, learnomy_lesson_content_types, and their neighbours, if you are extending behaviour in-process. The Abilities API if you are wiring an AI agent. All three are in the free plugin, and the integration docs walk through each one.
We built Learnomy because we wanted the LMS we would have wanted to build on ten years ago, part of the platform we wish we had started with. 1.4.0 is the release where that surface stopped being a plan and became something you can ship against today. Pull the update, open audit/manifest.json, and register something. If our own Pro team can add a whole math engine through one filter, we are genuinely curious what you will build.