{"slug":"lw-lms-rest-frontend","title":"lw-lms-rest-frontend","summary":"Build a custom frontend against LW LMS's headless `/wp-json/lms/v1` REST API in lw-lms v1.6.0. Use for React, Vue, Astro, mobile, or theme code that calls `/courses`, `/courses/{id}`, `/lessons/{id}`, `/progress`, `/progress/course/{id}`, or `/download/{id}` and must handle publi","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-16T14:52:01.321427Z","repo":{"url":"https://github.com/Lonsdale201/wp-agent-skills","stars":22,"forks":2,"license":"MIT","updatedAt":"2026-09-26T23:03:36Z"},"bodyHtml":"<hr>\n<h2>name: lw-lms-rest-frontend\ndescription: Build a custom frontend against LW LMS's headless <code>/wp-json/lms/v1</code> REST API in lw-lms v1.6.0. Use for React, Vue, Astro, mobile, or theme code that calls <code>/courses</code>, <code>/courses/{id}</code>, <code>/lessons/{id}</code>, <code>/progress</code>, <code>/progress/course/{id}</code>, or <code>/download/{id}</code> and must handle public course content, lesson/access gating, paid-course products/subscriptions/subscription_variations/memberships, custom paid-access decisions, progress payloads, downloads, and nonce/app-password auth.\nmetadata:\nwp-skills-author: \"Soczó Kristóf\"\nwp-skills-contact: \"mailto:lonsdale201@hotmail.com\"\nwp-skills-plugin: \"lw-lms\"\nwp-skills-plugin-version-tested: \"1.6.0\"\nwp-skills-php-min: \"8.2\"\nwp-skills-last-updated: \"2026-07-20\"</h2>\n<h1>LW LMS: REST frontend consumer</h1>\n<p>For frontend developers consuming LW LMS data: course catalog, course detail, lesson player, progress dashboard, and protected downloads. The core <code>lw-lms</code> plugin ships a headless REST API; it does not ship public templates, shortcodes, or blocks.</p>\n<blockquote>\n<p><strong>BETA NOTICE.</strong> The plugin README says the plugin is under active development and not recommended for production use. Snapshot the JSON shapes in tests and review <code>CHANGELOG.md</code> before upgrading. This skill is verified against local lw-lms <strong>v1.6.0</strong>.</p>\n</blockquote>\n<h2>Version deltas that matter</h2>\n<ul>\n<li><strong>v1.6.0</strong>: no route or JSON-schema change. A server-side <code>lw_lms_has_course_access</code> callback can now alter the final logged-in paid-course access decision after built-in access checks, which affects <code>access.has_access</code>, lesson accessibility, attachments, lesson detail, progress writes, and protected downloads.</li>\n<li><strong>v1.5.1</strong>: maintenance release, no functional REST changes.</li>\n<li><strong>v1.5.0</strong>: paid-course denied <code>access</code> payload can include <code>memberships</code> when WooCommerce Memberships is active and the course has <code>_lw_lms_membership_plan_ids</code>.</li>\n<li><strong>v1.4.0</strong>: course <code>content</code> in <code>/courses/{id}</code> is public marketing/about content. It is no longer gated behind <code>access.has_access</code>. Lesson content is still gated by <code>/lessons/{id}</code>.</li>\n<li><strong>v1.4.0</strong>: open-course lessons remain accessible to guests even if marked as preview.</li>\n<li><strong>v1.3.0</strong>: first logged-in access to a free course lazily writes a <code>source='free'</code> access row server-side; the REST consumer does not need to do anything special.</li>\n<li><strong>v1.2.15</strong>: denied paid-course <code>access</code> payload can include <code>subscription_variations</code>.</li>\n</ul>\n<h2>Misconception this skill corrects</h2>\n<blockquote>\n<p>\"I should use <code>course.content</code> as the access gate.\"</p>\n</blockquote>\n<p>Wrong for v1.4.0+. <code>CourseTransformer::transform_full()</code> always includes the course <code>content</code>; this is the public course description. Use <code>course.access.has_access</code> and each lesson's <code>accessible</code> flag for gating.</p>\n<pre><code>// WRONG: course content is public and is not proof of access.\nif (course.content) {\n    renderLessonPlayer();\n}\n\n// RIGHT: access comes from access.has_access and per-lesson accessible.\nif (course.access.has_access) {\n    return &lt;CourseContent html={course.content} progress={course.progress} /&gt;;\n}\n\nreturn &lt;PurchaseGate access={course.access} /&gt;;\n</code></pre>\n<p>Lesson body content still comes from <code>GET /lms/v1/lessons/{id}</code> and returns <code>403 forbidden</code> when <code>AccessChecker::has_lesson_access()</code> denies the request.</p>\n<p>In v1.6.0, a companion plugin may provide the final logged-in paid-course decision through <code>lw_lms_has_course_access</code>. Frontend code should continue to trust the server response; it must not reproduce membership or entitlement rules in JavaScript. Backend callbacks must be deterministic: the full-course transformer checks course access twice, so a changing result can make <code>access.has_access</code> disagree with lesson/attachment gating in one response.</p>\n<h2>REST API surface</h2>\n<p>Namespace: <code>lms/v1</code>, mounted at <code>/wp-json/lms/v1/...</code>.</p>\n<table>\n<thead>\n<tr>\n<th>Method</th>\n<th>Path</th>\n<th>Auth</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>GET</code></td>\n<td><code>/lms/v1/courses</code></td>\n<td>public</td>\n<td>Paginated public course list</td>\n</tr>\n<tr>\n<td><code>GET</code></td>\n<td><code>/lms/v1/courses/{id}</code></td>\n<td>public</td>\n<td>Single course detail; course content is public, lesson rows carry <code>accessible</code></td>\n</tr>\n<tr>\n<td><code>GET</code></td>\n<td><code>/lms/v1/lessons/{id}</code></td>\n<td>public route, access-gated body</td>\n<td>Single lesson; 403 without lesson access</td>\n</tr>\n<tr>\n<td><code>GET</code></td>\n<td><code>/lms/v1/progress</code></td>\n<td>logged-in</td>\n<td>Current user's all progress rows</td>\n</tr>\n<tr>\n<td><code>GET</code></td>\n<td><code>/lms/v1/progress/course/{id}</code></td>\n<td>logged-in</td>\n<td>Current user's progress rows for one course plus course summary progress</td>\n</tr>\n<tr>\n<td><code>POST</code></td>\n<td><code>/lms/v1/progress</code></td>\n<td>logged-in + lesson access</td>\n<td>Upsert lesson status</td>\n</tr>\n<tr>\n<td><code>GET</code></td>\n<td><code>/lms/v1/download/{id}</code></td>\n<td>public route, access-gated file</td>\n<td>Stream protected attachment binary</td>\n</tr>\n</tbody>\n</table>\n<h2>Detailed response contract</h2>\n<p>Read <a href=\"references/rest-response-contract.md\">references/rest-response-contract.md</a> when implementing request parameters, full JSON shapes, nullable fields, lesson errors, progress validation, or binary downloads. The route/auth/access decisions and client safety rules remain in this main skill.</p>\n<h2>Authentication</h2>\n<table>\n<thead>\n<tr>\n<th>Context</th>\n<th>Auth pattern</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Public catalog and course detail</td>\n<td>no auth</td>\n</tr>\n<tr>\n<td>Logged-in browser UI</td>\n<td>WordPress cookie + <code>X-WP-Nonce</code>, usually via <code>wp.apiFetch</code></td>\n</tr>\n<tr>\n<td>Headless/mobile</td>\n<td>Application Password Basic auth, or a vetted JWT/OAuth layer</td>\n</tr>\n<tr>\n<td>Downloads</td>\n<td>same auth context as the protected course/lesson</td>\n</tr>\n</tbody>\n</table>\n<p>For browser <code>fetch</code>, include both nonce and credentials:</p>\n<pre><code>await fetch('/wp-json/lms/v1/progress', {\n  method: 'POST',\n  credentials: 'same-origin',\n  headers: {\n    'Content-Type': 'application/json',\n    'X-WP-Nonce': wpApiSettings.nonce\n  },\n  body: JSON.stringify({ course_id: 42, lesson_id: 100, status: 'completed' })\n});\n</code></pre>\n<h2>Critical rules</h2>\n<ul>\n<li>Course <code>content</code> is public in v1.4.0+; never use it as the access gate.</li>\n<li>A server-side v1.6.0 paid-access filter may change access without creating an enrollment row; the REST response remains the authority for the client.</li>\n<li>Lesson detail and download endpoints are the protected surfaces.</li>\n<li>Iterate both <code>sections</code> and <code>lessons_without_section</code>.</li>\n<li>Render locked lessons disabled, not hidden.</li>\n<li><code>access.memberships</code> is available only when WooCommerce Memberships functions exist and linked plans are configured.</li>\n<li>List endpoint access info is intentionally small; build purchase gates from the single-course response.</li>\n<li><code>course_progress</code> uses <code>completed_lessons</code>, not <code>completed_count</code>.</li>\n<li><code>completed_at</code> is <code>null</code> for non-completed rows.</li>\n<li><code>content_raw</code> is editor-only source content; do not render it in public UI.</li>\n<li><code>download_url</code> is already a full REST URL; do not reconstruct it by concatenating <code>/wp-json</code>.</li>\n<li>The download endpoint returns binary data; do not call <code>response.json()</code> on it.</li>\n</ul>\n<h2>Common mistakes</h2>\n<pre><code>// WRONG: only iterating sections.\ncourse.sections.map(section =&gt; &lt;Section section={section} /&gt;);\n\n// RIGHT: include orphan lessons too.\n&lt;&gt;\n  {course.sections.map(section =&gt; &lt;Section key={section.id} section={section} /&gt;)}\n  {course.lessons_without_section.length &gt; 0 &amp;&amp; (\n    &lt;LessonGroup lessons={course.lessons_without_section} /&gt;\n  )}\n&lt;/&gt;\n</code></pre>\n<pre><code>// WRONG: old pre-1.4 assumption.\nconst canAccess = Boolean(course.content);\n\n// RIGHT.\nconst canAccess = course.access.has_access;\n</code></pre>\n<pre><code>// WRONG: progress key from stale docs.\n&lt;ProgressBar value={data.course_progress.completed_count} /&gt;\n\n// RIGHT.\n&lt;ProgressBar value={data.course_progress.completed_lessons} /&gt;\n</code></pre>\n<pre><code>// WRONG: POST body missing course_id.\nfetch('/wp-json/lms/v1/progress', {\n  method: 'POST',\n  body: JSON.stringify({ lesson_id: 100, status: 'completed' })\n});\n\n// RIGHT.\nfetch('/wp-json/lms/v1/progress', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': wpApiSettings.nonce },\n  credentials: 'same-origin',\n  body: JSON.stringify({ course_id: 42, lesson_id: 100, status: 'completed' })\n});\n</code></pre>\n<h2>Cross-references</h2>\n<ul>\n<li>Use <code>lw-lms-backend-extend</code> for hooks, access/progress repositories, and companion-plugin backend integration.</li>\n<li>Use <code>lw-lms-abilities</code> for admin/agent <code>lw-lms/*</code> Abilities API calls; those are not the learner-facing REST endpoints.</li>\n<li>Use <code>lw-lms-wp-cli-operations</code> for operational WP-CLI course, lesson, enrollment, revoke, and force-complete commands.</li>\n<li>Use <code>lw-lms-learndash-migration</code> for the one-time LearnDash migration command.</li>\n</ul>\n<h2>What this skill does NOT cover</h2>\n<ul>\n<li>A separate frontend plugin. This skill documents the core <code>lw-lms</code> REST contract only.</li>\n<li>Payment processing. Link to WooCommerce products/subscriptions/membership join URLs and let WooCommerce handle checkout.</li>\n<li>Custom REST route registration. Use normal WordPress <code>register_rest_route()</code> patterns in a companion plugin.</li>\n<li>Server-rendered theme templates. You can consume REST server-side, but direct CPT/meta queries are usually simpler in PHP templates.</li>\n</ul>\n<h2>References</h2>\n<ul>\n<li>REST namespace and route registration: <code>includes/Api/RestApi.php</code>.</li>\n<li>Course list/detail routes: <code>includes/Api/Controllers/CoursesController.php</code>.</li>\n<li>Lesson route and 403 behavior: <code>includes/Api/Controllers/LessonsController.php</code>.</li>\n<li>Progress routes and cross-validation: <code>includes/Api/Controllers/ProgressController.php</code>.</li>\n<li>Download route and binary response: <code>includes/Api/Controllers/DownloadController.php</code>.</li>\n<li>Course response shape and public <code>content</code>: <code>includes/Api/Transformers/CourseTransformer.php</code>.</li>\n<li>Lesson response shape: <code>includes/Api/Transformers/LessonTransformer.php</code>.</li>\n<li>Progress row shape: <code>includes/Api/Transformers/ProgressTransformer.php</code>.</li>\n<li>Paid access payload including memberships: <code>includes/Access/AccessChecker.php</code>, <code>includes/Access/MembershipChecker.php</code>.</li>\n<li>Official documentation: <a href=\"https://github.com/lwplugins/lw-lms\">https://github.com/lwplugins/lw-lms</a></li>\n<li>Verified source paths:\n<ul>\n<li><code>wp-content/plugins/lw-lms/includes/Access/SubscriptionVariationChecker.php</code></li>\n<li><code>wp-content/plugins/lw-lms/includes/Access/WooCommerceChecker.php</code></li>\n<li><code>wp-content/plugins/lw-lms/includes/Meta/VideoParser.php</code></li>\n<li><code>wp-content/plugins/lw-lms/includes/Progress/ProgressCalculator.php</code></li>\n<li><code>wp-content/plugins/lw-lms/CHANGELOG.md</code></li>\n</ul>\n</li>\n</ul>\n","files":[{"path":"references/rest-response-contract.md","sizeBytes":3758,"isText":true},{"path":"SKILL.md","sizeBytes":6213,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-29T23:41:15.558959Z","sha256":"36BFA03ED13F5B0170BDE6B855720C23EBC2D85E6EF30CCE0A90D43C6B6E4677","sizeBytes":4749},"review":null,"source":{"repositoryUrl":"https://github.com/Lonsdale201/wp-agent-skills","path":"lw-plugins/lw-lms-rest-frontend","license":"MIT","commit":"c51b571a259f0c4b5f5c0a3bc50ed580c6851f98","subtreeSha":"787AE6EEF2F3FDE29B85D83616BD2C4A78DA8BABE66DF56B24F1CAB3E512155B","lastSyncedAt":"2026-09-29T23:33:03.303675Z"},"reviewedAt":"2026-09-29T23:45:36.293911Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/lw-plugins/lw-lms-rest-frontend"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart"},{"target":"git","command":"git clone https://github.com/Lonsdale201/wp-agent-skills.git"}]}