API reference
Every route the Learnio API serves, who may call it, and how sign-in, permissions, errors and pagination work.
For the Full Stack package
Base URL and response format
Every route is served under the /api prefix. Locally the base URL is http://localhost:8000/api. On a server it is your API's address followed by /api, the same value both frontends read from NEXT_PUBLIC_API_BASE_URL (Environment variables).
The API serves two audiences that never share a token. Students use the routes under /api/users/<area> and /api/shop, listed in the student sections below. The admin dashboard uses every other route, including /api/users itself and /api/users/<username>, which manage student accounts.
Request bodies are JSON, except the file upload. Every answer comes in the same envelope:
{
"success": true,
"data": { },
"message": ""
}data holds the result. message is a short sentence some writes fill in, translated to the request's language, and empty otherwise. The health check needs no token and shows the envelope:
curl http://localhost:8000/api/health{"success":true,"data":{"status":"ok"},"message":""}It answers 503 while the database cannot be reached or holds no tables, so a load balancer can use it as a readiness probe.
Errors
A failed request answers with the matching HTTP status and the same envelope, with success: false, data: null and, for validation, the problems per field, keyed by the field's name (a field inside a nested object by its dotted path):
curl -X POST http://localhost:8000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"not-an-email","password":"x"}'{
"success": false,
"data": null,
"message": "Enter a valid email address (example@domain.com).",
"errors": {
"email": ["Enter a valid email address (example@domain.com)."]
}
}| Status | When |
|---|---|
400 | A field failed validation, a query value is unusable, or the body holds a field the route does not accept. Unknown fields are refused, not ignored. |
401 | The token is missing, expired, or belongs to the other audience. Also a failed sign-in, whether the email has no account or the password is wrong. |
403 | The admin's roles do not grant the route's permission, or an instructor account reached outside its own courses. |
404 | No such record. |
409 | The write clashes with an existing record, such as an email or slug already in use. |
429 | Too many attempts from one address on a sign-in, sign-up or password route. The Retry-After header says how many seconds to wait. |
500 | An unexpected failure. The message stays generic and the details go to the API's log. |
Successful writes answer 201 for POST and 200 for the other methods.
Pagination and sorting
Lists come in two shapes, one per audience. The student routes answer in the shape the student site reads; the admin routes answer in the shape the dashboard reads.
| Student and public lists | Admin lists | |
|---|---|---|
| Page | page, from 1 | page, from 1 |
| Page size | page_count (courses, blog, enrolments, reviews) or per_page (instructors). Courses default to the courses_per_page setting, enrolments to 100. | page_count, 15 by default (10 for admins). Orders use limit. |
| Sort | Courses only: sort_by and sort_dir (asc or desc) | sort and order (asc or desc). Orders use sort_by and sort_order. |
| Default order | Courses newest-updated first, blog newest-published first | Newest-updated first, ties broken by id. Categories and instructors keep their curated order. |
| Answer | data, current_page, last_page, per_page, total, from, to, plus page links | data, page, limit, total, totalPages |
A page holds at most 100 rows: a larger page size is read as 100, and a missing or unreadable one falls back to the list's default. The same goes for page, which falls back to 1.
sort only accepts the columns each list allows. An unknown key falls back to the default order instead of failing, so an old bookmark still loads.
The public course list sorts by id, price, discount_percentage, rating, duration, level, language, created_at or updated_at, and filters by title, category_id, instructor_id and type (live or recorded).
Language
Send the reader's language in the Accept-Language header. The API speaks the languages listed in back-end/src/i18n/locales.ts, en and ar as shipped. It reads the first tag, so ar-SA is Arabic, and a request that names no supported language is answered in the default_locale setting's language. There is no language query parameter.
- Error messages and the
messageof a successful write are translated. - Text stored in both languages, such as course titles and descriptions, comes back as
{ "en": "...", "ar": "..." }and the client picks one. - A few student routes answer in the requested language only: the enrolment list, a course's content and the search.
- Checkout takes the language from its body (
locale, one of the supported languages), so the receipt matches the page the student bought on.
Authentication
Students and admins sign in at different endpoints, against different tables, and receive tokens only their own routes accept.
| Audience | Sign in | Token in the answer | Accepted on |
|---|---|---|---|
| Student | POST /api/users/auth/login | data.token, with data.user | Student routes |
| Admin | POST /api/auth/login | data.access_token, with data.admin (roles and permissions) | Admin routes |
- Both are JWTs signed with
JWT_SECRET. Send them asAuthorization: Bearer <token>. - A token lasts for
JWT_EXPIRATION,7dby default (Environment variables). The admin sign-in reports the same value asexpires_in. - There is no refresh endpoint. When a token expires, the next request answers
401and the client signs in again. - Signing out (
POST /api/users/auth/logout) only tells the client to drop its token. Nothing is revoked on the server, so a token stays valid until it expires. - A student token is refused on admin routes and an admin token on student routes, although both use the same secret.
- A failed sign-in answers
401with one message, whether the email has no account or the password is wrong, so the form cannot be used to find out who has an account. - The sign-in, sign-up and password routes accept 10 attempts a minute from one address, counted per route. Past that they answer
429with aRetry-Afterheader. The count is kept in the API's memory, so it starts again on a restart and is counted separately by each instance.
Sign in as the seeded Super Admin
Terminalcurl -X POST http://localhost:8000/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"admin@learnio.com","password":"Admin@123"}'Response{ "success": true, "data": { "access_token": "eyJhbGciOiJIUzI1NiIs...", "token_type": "Bearer", "expires_in": "7d", "admin": { "id": 1, "email": "admin@learnio.com", "instructor_id": null, "roles": [{ "id": 1, "name": "Super Admin", "guard_name": "web" }], "permissions": ["admins.view", "admins.create", "..."] } }, "message": "Signed in successfully." }Call a protected route with the token
Terminalcurl "http://localhost:8000/api/courses?page=1&page_count=5" \ -H "Authorization: Bearer <access_token>"Expected result: The first five courses, in the admin list shape.
Sign in as the seeded student
The sample data also holds a student,
demo@learnio.comwith the passwordDemo@123:Terminalcurl -X POST http://localhost:8000/api/users/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"demo@learnio.com","password":"Demo@123"}'Then pass
data.tokenthe same way, for example toGET /api/users/enrollments.
Change the seeded passwords
The sample data's accounts use published passwords: Admin@123 for the Super Admin, admin123 for the other staff and Demo@123 for the demo student. Change them, or start from empty tables, before a site goes live.
Permissions
Every admin route checks the token first, then the permission it names. Permissions are named <module>.<action>, and an admin holds every permission of every role they have. Where a route names several, any one of them is enough. In the admin tables below, a permission name in the Who can call it column means an admin whose roles grant it.
Running the seeder again creates the roles that are missing and leaves existing roles' permissions as they are, so changes made in the dashboard survive. Super Admin is the exception: it receives any permission it does not hold yet.
| Module | Permissions |
|---|---|
| Admins | admins.view, admins.create, admins.edit, admins.delete, admins.assign_roles |
| Roles | roles.view, roles.create, roles.edit, roles.delete, roles.assign_permissions |
| Settings | settings.view, settings.edit |
| Students | students.view, students.create, students.update, students.delete, students.restore, students.verify |
| Categories | categories.view, categories.create, categories.edit, categories.delete, categories.restore |
| Courses | courses.view, courses.create, courses.edit, courses.delete, courses.restore |
| Curriculum | curriculum.view, curriculum.create, curriculum.edit, curriculum.delete |
| Instructors | instructors.view, instructors.create, instructors.edit, instructors.delete, instructors.restore |
| Enrolments | enrollments.view, enrollments.create, enrollments.edit, enrollments.delete |
| Orders | orders.view, orders.create, orders.edit, orders.delete |
| Reviews | reviews.view, reviews.edit, reviews.delete, reviews.restore |
| Live classes | live_sessions.view, live_sessions.create, live_sessions.edit, live_sessions.delete |
| Assignments | assignments.view, assignments.create, assignments.edit, assignments.delete |
| Blog | blog.view, blog.create, blog.edit, blog.delete, blog.restore |
| AI assistant | ai_chat.use, ai_chat.view_models |
Every read permission ends in .view and nothing else does: the seeded Viewer role is built from that rule. The roles the sample data creates:
| Role | Grants |
|---|---|
| Super Admin | Everything |
| Admin | Everything except roles.*, admins.* and settings.edit |
| Manager | Courses, curriculum, categories, instructors, enrolments and orders without deletes, plus students.view, reviews.view and reviews.edit |
| Editor | blog.*, reviews.view, reviews.edit, categories.view, courses.view, instructors.view |
| Viewer | Every .view permission |
| Instructor | Live classes, assignments and curriculum, courses.view, courses.create, courses.edit, and a read of students, enrolments and reviews |
An admin account linked to an instructor carries an instructor_id in its sign-in answer, and is held to its own courses on top of its permissions: lists show only those courses and their students, and a write outside them answers 403.
When an admin's roles change, the dashboard is told over the /auth socket (event permissions-updated) so it can refresh GET /api/auth/me. That socket accepts admin tokens only.
Public catalogue
No token needed. These are the routes the student site's public pages call.
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/health | Anyone | Readiness check |
GET | /api/users/courses | Anyone | Lists the published public courses, paginated (page_count, the courses_per_page setting by default), with filters and sorting |
GET | /api/users/courses/categories | Anyone | Categories that hold at least one public course, with their counts |
GET | /api/users/courses/:slug | Anyone | One published public course with its sections and lessons. A draft answers 404. Only free-preview lessons carry their content and video. |
GET | /api/users/courses/:slug/reviews | Anyone | The course's published reviews, most recently updated first, paginated (page_count, 5 by default) |
GET | /api/users/Instructors | Anyone | Instructor directory, paginated (per_page, 8 by default), filter by specialty |
GET | /api/users/Instructors/:username | Anyone | One instructor with their public courses |
GET | /api/users/blog | Anyone | Published posts, newest first, paginated (page_count, 9 by default), filter by tag |
GET | /api/users/blog/tags | Anyone | Every tag in use, with how many posts carry it |
GET | /api/users/blog/:slug | Anyone | One published post |
GET | /api/users/platform/figures | Anyone | The home page's counts: students, courses, instructors, countries and satisfaction |
GET | /api/categories | Anyone | Category list, paginated, with search, is_active, parent_id, has_courses |
GET | /api/categories/roots | Anyone | Top-level categories |
GET | /api/categories/slug/:slug | Anyone | One category by slug |
GET | /api/categories/:id | Anyone | One category by id |
GET | /api/helpers/countries | Anyone | Countries for a select field, in the request's language |
GET | /api/shop/payment-methods | Anyone | The payment methods the checkout should offer, and which of them are hosted |
The capital I in /api/users/Instructors is the path the student site calls; matching is not case sensitive.
Student sign-up and sign-in
The student site uses the routes under /api/users/auth. Registration signs the student in straight away and mails a confirmation link; an unconfirmed account can still browse and buy.
| Method | Path | Who can call it | What it does |
|---|---|---|---|
POST | /api/users/auth/register | Anyone | Creates the account (first_name, last_name, email, password of 8 or more, optional phone) and signs in. data.verification_email_sent says whether the mail left. |
POST | /api/users/auth/login | Anyone | Signs in with email and password, answers token and user |
POST | /api/users/auth/logout | Anyone | Nothing to revoke; lets the client clear its token |
POST | /api/users/auth/resend | Anyone | Mails the confirmation link again to email |
POST | /api/users/auth/verify-email | Anyone | Confirms the address with the token from the link (valid 24 hours) |
POST | /api/users/auth/forgot-password | Anyone | Mails a reset link to email (valid 1 hour) |
POST | /api/users/auth/reset-password | Anyone | Sets a new password with the token from the link |
GET | /api/users/auth/me | Student | The signed-in student |
resend and forgot-password answer the same way whether or not the address has an account, so they cannot be used to find out who is registered. Sign-in, registration, resend, forgot-password and reset-password are rate limited (Authentication).
A second set of student routes under /api/students/auth works on the same accounts, answering token and user too. The student site does not use it; prefer /api/users/auth.
| Method | Path | Who can call it | What it does |
|---|---|---|---|
POST | /api/students/auth/register | Anyone | Creates a student account and signs in, without a confirmation mail |
POST | /api/students/auth/login | Anyone | Signs in |
GET | /api/students/auth/me | Student | The signed-in student's profile |
PATCH | /api/students/auth/me | Student | Updates name, email or phone |
PATCH | /api/students/auth/me/password | Student | Changes the password |
POST | /api/students/auth/forgot-password | Anyone | Mails a reset link to email, answering the same way whether or not the address has an account |
POST | /api/students/auth/reset-password | Anyone | Sets a new password with a reset token |
Student profile, courses and progress
Every route here needs a student token. Courses and lessons are addressed by their code, not their numeric id.
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/users/profile | Student | The signed-in student |
PATCH | /api/users/profile | Student | Updates first_name, last_name, email or phone |
PATCH | /api/users/profile/password | Student | Changes the password (the current one and a new one of 8 or more) |
GET | /api/users/dashboard/overview | Student | Everything the dashboard's home screen shows, in one call |
GET | /api/users/dashboard/kpis | Student | The student's headline figures |
GET | /api/users/dashboard/radar | Student | The skills chart, from the dashboard_radar_metrics setting |
GET | /api/users/enrollments | Student | The student's courses, paginated (page_count, 100 by default), filter by type, search, status |
GET | /api/users/enrollments/courses/:code | Student | A held course's full content, with each lesson's status and the progress |
PATCH | /api/users/enrollments/courses/:code | Student | Records the last lesson opened (last_accessed_lesson_id) |
POST | /api/users/enrollments/lessons/:lessonCode/complete | Student | Marks a lesson complete and updates the course progress |
GET | /api/users/enrollments/lessons/:lessonCode/note | Student | The student's note on a lesson |
PUT | /api/users/enrollments/lessons/:lessonCode/note | Student | Saves the note (body) |
GET | /api/users/search | Student | Global search (q, limit per kind): the student's courses, the catalogue, instructors and more |
GET | /api/users/courses/:courseId/review | Student | The student's own review of the course, or null when there is none |
PUT | /api/users/courses/:courseId/review | Student | Writes or replaces the student's review: rating from 1 to 5, optional comment up to 2,000 characters |
DELETE | /api/users/courses/:courseId/review | Student | Withdraws the student's review |
The review routes take the course's numeric id, and each student has one review per course. Only a student enrolled on the course can write one; a cancelled enrolment answers 403.
- A review is
publishedstraight away, orpendinguntil a moderator approves it when thereviews_require_approvalsetting istrue. Editing a review a moderator hid sends it back topending. - A review a moderator deleted comes back with
status: removedand can no longer be edited or withdrawn (403). - Every write recomputes the course's rating and review count, and those of its instructor.
- A new review, and an edit that waits for approval, notifies the staff who hold
reviews.view.
Quizzes, assignments and calendar
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/users/quizzes?course_id= | Student | Quizzes of a course the student holds |
GET | /api/users/quizzes/:id | Student | One quiz with its questions, without the answers |
GET | /api/users/quizzes/:id/attempts | Student | The student's past attempts |
POST | /api/users/quizzes/:id/attempts | Student | Submits answers (question id to chosen option ids) and returns the score |
GET | /api/users/dashboard/calendar | Student | Classes and assignment deadlines between from and to (ISO dates, 7 days by default, 92 at most) |
POST | /api/users/dashboard/calendar/assignments/:id/submit | Student | Hands in an assignment (body) |
DELETE | /api/users/dashboard/calendar/assignments/:id/submit | Student | Withdraws a submission |
The calendar's live class routes are described with Live classes.
Student messages and notifications
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/users/conversations | Student | The student's conversations with their instructors |
POST | /api/users/conversations | Student | Opens, or returns, the conversation with the instructor of course_id |
GET | /api/users/conversations/:id/messages | Student | Messages in one conversation |
POST | /api/users/conversations/:id/messages | Student | Sends a message: body, an image (attachment_url, attachment_name, attachment_type), or both |
PATCH | /api/users/conversations/:id/read | Student | Marks the conversation read |
PATCH | /api/users/conversations/:id/unread | Student | Marks it unread |
GET | /api/users/notifications | Student | The student's notifications |
PATCH | /api/users/notifications/:id/read | Student | Marks one read |
PATCH | /api/users/notifications/read-all | Student | Marks all read |
DELETE | /api/users/notifications/:id | Student | Deletes one |
New notifications are also pushed over Socket.IO, on the /notifications namespace at the API's address without /api. Pass the token as auth.token in the handshake and listen for notification:created. Admins use the same socket with their own token.
Checkout and payments
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/shop/payment-methods | Anyone | methods the checkout offers and which are hosted |
POST | /api/shop/checkout | Student | Buys course_id, or up to 50 course_ids in one charge, with payment_method (card or paypal) and locale |
POST | /api/shop/orders/:reference/settle | Student | Finishes a hosted payment when the buyer returns. Needed for PayPal; does nothing for Stripe. |
GET | /api/shop/orders | Student | The student's orders |
POST | /api/webhooks/payments/stripe | Stripe, signed | Settles, fails or refunds orders from Stripe's events |
POST | /api/webhooks/payments/paypal | PayPal, signed | Settles, fails or refunds orders from PayPal's events |
When the checkout answers with redirect_url, send the buyer there and treat the order as pending: the seat is granted when the processor's webhook confirms the payment, not on the return. Each webhook checks the processor's signature and answers 401 when it does not match. Events are applied once, however often the processor retries.
A free course (price 0) goes through the same route and never reaches a processor, so it works with no payment keys at all. The order is written as paid with gateway: "free" and redirect_url: null, the seat is granted at once, and the student receives the enrolment email without a receipt. Free orders count as enrolments, not sales: they are left out of the average order value.
Setting up the processors and their webhook subscriptions: Payments.
Admin sign-in and staff accounts
| Method | Path | Who can call it | What it does |
|---|---|---|---|
POST | /api/auth/login | Anyone | Signs an admin in, answers access_token, roles and permissions. Rate limited. |
GET | /api/auth/me | Any signed-in admin | The signed-in admin with roles and permissions |
GET | /api/admins | admins.view | Staff list, filter by email, name, phone, role_id |
GET | /api/admins/statistics | admins.view | Staff counts |
GET | /api/admins/:id | admins.view | One admin |
POST | /api/admins | admins.create | Creates an admin |
PATCH | /api/admins/:id | admins.edit | Updates an admin |
PATCH | /api/admins/:id/roles | admins.assign_roles | Replaces the admin's roles (role_ids) |
DELETE | /api/admins/:id | admins.delete | Deletes an admin |
PATCH | /api/admins/profile | Any signed-in admin | Updates the signed-in admin's own name, contact details and picture |
PATCH | /api/admins/profile/password | Any signed-in admin | Changes the signed-in admin's own password |
An email address belongs to one admin, including a deleted one: creating an admin, or changing an email to one already used, answers 409.
Roles
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/roles | roles.view | Roles, paginated when page is sent, filter by name, guard_name, created_from, created_to |
GET | /api/roles/statistics | roles.view | Role counts |
GET | /api/roles/select | roles.view | Every role, for a picker |
GET | /api/roles/permissions | roles.view | Every permission, grouped by module |
GET | /api/roles/:id | roles.view | One role with its permissions |
POST | /api/roles | roles.create | Creates a role (name) |
PUT | /api/roles/:id | roles.edit | Renames a role |
POST | /api/roles/:id/permissions | roles.assign_permissions | Replaces the role's permissions with permissions, a list of names |
DELETE | /api/roles/:id | roles.delete | Deletes a role |
Students
Student accounts are addressed by username. Deleting one is a soft delete that can be restored.
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/users | students.view | Students, with search, email, phone, country_id, username, first_name, last_name, from_date, to_date, verified |
GET | /api/users/statistic | students.view | Student counts |
GET | /api/users/:username | students.view | One student |
POST | /api/users | students.create | Creates a student |
PATCH | /api/users/:username | students.update | Updates a student |
PATCH | /api/users/:username/change-password | students.update | Sets a student's password |
POST | /api/users/:username/resend-verification-email | students.update | Mails the confirmation link again. data.sent is false and data.simulated true when no mail provider is set and the link went to the log. 409 if already confirmed, 429 when the address had too many, 503 when the provider refused. |
POST | /api/users/:username/make-verified | students.verify | Marks the email confirmed |
POST | /api/users/:username/make-unverified | students.verify | Marks the email unconfirmed |
DELETE | /api/users/:username | students.delete | Moves the student to the recycle bin |
GET | /api/users/deleted | students.view, students.delete or students.restore | Deleted students |
GET | /api/users/deleted/:username | students.view, students.delete or students.restore | One deleted student |
POST | /api/users/deleted/:username/restore | students.restore | Restores a deleted student |
Instructors
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/instructors | instructors.view | Instructors, with search, specialty, status, is_featured |
GET | /api/instructors/statistic | instructors.view | Instructor counts |
GET | /api/instructors/username/:username | instructors.view | One instructor by username |
GET | /api/instructors/:id | instructors.view | One instructor |
POST | /api/instructors | instructors.create | Creates an instructor |
PATCH | /api/instructors/:id | instructors.edit | Updates an instructor |
DELETE | /api/instructors/:id | instructors.delete | Moves an instructor to the recycle bin |
GET | /api/instructors/deleted | instructors.view, instructors.delete or instructors.restore | Deleted instructors |
POST | /api/instructors/deleted/:id/restore | instructors.restore | Restores one |
An instructor account messages its students through these routes, which answer 403 for an admin not linked to an instructor:
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/conversations | Instructor account | The instructor's conversations |
GET | /api/conversations/with/:userId | Instructor account | Courses the instructor shares with a student, to start a conversation from |
POST | /api/conversations | Instructor account | Opens, or returns, a conversation with user_id, optionally about course_id |
GET | /api/conversations/:id/messages | Instructor account | Messages in one conversation |
POST | /api/conversations/:id/messages | Instructor account | Sends a message |
PATCH | /api/conversations/:id/read | Instructor account | Marks it read |
PATCH | /api/conversations/:id/unread | Instructor account | Marks it unread |
Categories
The reads are open to anyone and listed with the Public catalogue. Writes need a permission:
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/categories/statistic | categories.view | Category counts |
POST | /api/categories | categories.create | Creates a category |
PATCH | /api/categories/:id | categories.edit | Updates a category |
DELETE | /api/categories/:id | categories.delete | Moves a category to the recycle bin |
GET | /api/categories/deleted | categories.delete or categories.restore | Deleted categories |
POST | /api/categories/deleted/:id/restore | categories.restore | Restores one |
Courses
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/courses | courses.view | Courses, with search, category_id, instructor_id, status, type, is_public, is_featured |
GET | /api/courses/statistic | courses.view | Course counts |
GET | /api/courses/slug/:slug | courses.view | One course by slug |
GET | /api/courses/:id | courses.view | One course |
POST | /api/courses | courses.create | Creates a course |
PATCH | /api/courses/:id | courses.edit | Updates a course |
DELETE | /api/courses/:id | courses.delete | Moves a course to the recycle bin |
GET | /api/courses/deleted | courses.delete or courses.restore | Deleted courses |
POST | /api/courses/deleted/:id/restore | courses.restore | Restores one |
Curriculum: sections, lessons and resources
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/courses/:courseId/sections | curriculum.view | A course's sections |
POST | /api/courses/:courseId/sections | curriculum.create | Adds a section |
POST | /api/courses/:courseId/sections/reorder | curriculum.edit | Sets the section order from ids |
GET | /api/sections/:id | curriculum.view | One section |
PATCH | /api/sections/:id | curriculum.edit | Updates a section |
DELETE | /api/sections/:id | curriculum.delete | Deletes a section |
GET | /api/sections/:sectionId/lessons | curriculum.view | A section's lessons |
POST | /api/sections/:sectionId/lessons | curriculum.create | Adds a lesson |
POST | /api/sections/:sectionId/lessons/reorder | curriculum.edit | Sets the lesson order from ids |
GET | /api/lessons/:id | curriculum.view | One lesson |
PATCH | /api/lessons/:id | curriculum.edit | Updates a lesson |
DELETE | /api/lessons/:id | curriculum.delete | Deletes a lesson |
GET | /api/lessons/:lessonId/resources | curriculum.view | A lesson's downloadable resources |
POST | /api/lessons/:lessonId/resources | curriculum.edit | Attaches a resource |
PATCH | /api/lesson-resources/:id | curriculum.edit | Updates a resource |
DELETE | /api/lesson-resources/:id | curriculum.edit | Removes a resource |
Quizzes and assignments
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/quizzes?course_id= | courses.view | A course's quizzes (course_id is required) |
GET | /api/quizzes/:id | courses.view | One quiz with its questions and answers |
POST | /api/quizzes | courses.create | Creates a quiz |
PATCH | /api/quizzes/:id | courses.edit | Updates a quiz |
DELETE | /api/quizzes/:id | courses.delete | Deletes a quiz |
POST | /api/quizzes/:id/questions | courses.edit | Adds a question |
PATCH | /api/quizzes/:id/questions/:questionId | courses.edit | Updates a question |
DELETE | /api/quizzes/:id/questions/:questionId | courses.edit | Deletes a question |
GET | /api/assignments | assignments.view | Assignments, with search, course_id, lesson_id, status, from, to |
GET | /api/assignments/:id | assignments.view | One assignment |
GET | /api/assignments/:id/submissions | assignments.view | The submissions waiting to be marked |
PATCH | /api/assignments/submissions/:submissionId/grade | assignments.edit | Marks a submission |
POST | /api/assignments | assignments.create | Creates an assignment |
PATCH | /api/assignments/:id | assignments.edit | Updates an assignment |
DELETE | /api/assignments/:id | assignments.delete | Deletes an assignment |
Enrolments and orders
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/enrollments | enrollments.view | Enrolments, with search, course_id, user_id, status, date_from, date_to |
GET | /api/enrollments/statistic | enrollments.view | Enrolment counts |
GET | /api/enrollments/:id | enrollments.view | One enrolment |
POST | /api/enrollments | enrollments.create | Enrols a student by hand |
PATCH | /api/enrollments/:id | enrollments.edit | Updates an enrolment |
DELETE | /api/enrollments/:id | enrollments.delete | Removes an enrolment |
GET | /api/orders | orders.view | Orders, with search, status, payment_method, course_id, user_id, date_from, date_to, page, limit, sort_by, sort_order |
GET | /api/orders/revenue | orders.view | Revenue between date_from and date_to. The average order value leaves out free orders. |
GET | /api/orders/revenue/series | orders.view | Daily revenue for the last days (30 by default, 365 at most) |
GET | /api/orders/:id | orders.view | One order |
POST | /api/orders | orders.create | Records a seat paid for elsewhere |
PATCH | /api/orders/:id | orders.edit | Updates an order |
DELETE | /api/orders/:id | orders.delete | Deletes an order |
An order's status is pending, paid, failed or refunded. Its gateway names the processor that took the payment, demo for the built-in simulator, or free for a course that cost nothing.
Reviews
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/course-reviews | reviews.view | Reviews, with search, course_id, user_id, rating, status |
GET | /api/course-reviews/statistic | reviews.view | Review counts |
GET | /api/course-reviews/:id | reviews.view | One review |
PATCH | /api/course-reviews/:id | reviews.edit | Moderates a review: status is published, pending or hidden |
DELETE | /api/course-reviews/:id | reviews.delete | Moves a review to the recycle bin |
GET | /api/course-reviews/deleted | reviews.delete or reviews.restore | Deleted reviews |
POST | /api/course-reviews/deleted/:id/restore | reviews.restore | Restores one |
Students write their own reviews through the student routes, and the course page reads the published ones from the Public catalogue. Every moderation change recomputes the course's and the instructor's rating.
Blog
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/blog | blog.view | Posts in every status, with search, category_slug, status, tag |
GET | /api/blog/statistic | blog.view | Post counts |
GET | /api/blog/slug/:slug | blog.view | One post by slug |
GET | /api/blog/:id | blog.view | One post |
POST | /api/blog | blog.create | Creates a post |
PATCH | /api/blog/:id | blog.edit | Updates a post |
DELETE | /api/blog/:id | blog.delete | Moves a post to the recycle bin |
GET | /api/blog/deleted | blog.delete or blog.restore | Deleted posts |
POST | /api/blog/deleted/:id/restore | blog.restore | Restores one |
Settings
Platform settings are stored as key and value pairs and addressed by key.
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/settings | settings.view | Settings, with search, category, type |
GET | /api/settings/:key | settings.view | One setting |
PATCH | /api/settings/:key | settings.edit | Changes a setting's value |
DELETE | /api/settings/:key | settings.edit | Deletes a setting |
The seeder creates these six, and each is read by the API or the student dashboard:
| Key | Seeded value | What it does |
|---|---|---|
site_name | Learnio | The product name written into every email the API sends |
default_locale | en | The language used when a request names no supported language. A value that is not a supported language is refused with 400. |
support_email | support@learnio.com | The reply-to address of every email, so a reply reaches a person |
courses_per_page | 12 | The public catalogue's page size when the request sets none (1 to 100) |
reviews_require_approval | false | When true, a student's review waits as pending until a moderator publishes it |
dashboard_radar_metrics | A JSON map of metric to score | The skills chart on the student dashboard |
A change takes effect on the next request or email, with no restart. Running the seeder again adds a missing setting but never overwrites a value already saved.
Media uploads
| Method | Path | Who can call it | What it does |
|---|---|---|---|
POST | /api/helpers/upload | Any signed-in admin, or a student | Uploads one file to the media bucket and answers its addresses |
Send multipart/form-data with the file in file, up to 150 MB, and optionally path (the folder, uploads by default), for (an image size preset) and type (video to skip image processing). Images are resized into several versions; video (mp4, webm, mov, m4v) is stored as it is. It needs the bucket settings in Media storage.
The route needs an admin or a student token, checked before the file is read. path must be one of the folders listed in back-end/src/modules/helpers/upload/upload-folders.constants.ts; any other answers 400. An admin may upload to uploads, admins/profile, admins/profiles, users/profiles, blog, categories, courses, instructors, messages and ai-chat. A student may upload only to messages, for the images they attach to a conversation. A new screen that uploads to a folder of its own needs that folder added to the list.
curl -X POST http://localhost:8000/api/helpers/upload \
-H "Authorization: Bearer <access_token>" \
-F "file=@cover.jpg" -F "path=courses"Notifications, search and statistics
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/notifications | Any signed-in admin | The signed-in admin's notifications and unread count |
PATCH | /api/notifications/read-all | Any signed-in admin | Marks all read, answers the whole feed |
PATCH | /api/notifications/:id/read | Any signed-in admin | Marks one read, answers the whole feed |
DELETE | /api/notifications/:id | Any signed-in admin | Deletes one, answers the whole feed |
GET | /api/search | Any signed-in admin | Global search (q, limit per kind). Only the kinds the admin's permissions allow are searched. |
GET | /api/statistics/trends | courses.view | The figures behind every stat card, in one call |
GET | /api/statistics/overview | courses.view | The dashboard home's overview, over days, with limit rows per list |
An admin only ever reads and changes their own notifications: the routes take the admin from the token. The API writes one for each of these events, to every admin holding the permission the linked screen needs:
| Type | When | Who receives it |
|---|---|---|
enrollment_created | A student is given a seat, by a purchase, a free course or staff | enrollments.view |
course_completed | A student finishes a course | enrollments.view |
course_review_submitted | A student leaves a review, or edits one that waits for approval | reviews.view |
course_published | A course is published | courses.view |
student_registered | A student creates an account | students.view |
An admin account linked to an instructor hears only about that instructor's own courses, and not about new sign-ups. Each one is also pushed over the /notifications socket as it is saved.
Live classes
Included with your purchase. Sign in to read, or open it in your download.
The live class routes: scheduling sessions, and the join tokens students and hosts receive for a Jitsi Meet room.
Certificates
Included with your purchase. Sign in to read, or open it in your download.
The certificate routes: how a student claims one for a finished course, and the public verification address.
AI assistant
Included with your purchase. Sign in to read, or open it in your download.
The AI assistant's routes: streaming a reply, the model list, and saved chat sessions.
Assistant chat history
Included with your purchase. Sign in to read, or open it in your download.
Saving, listing and renaming the assistant's conversations.
MCP server
Included with your purchase. Sign in to read, or open it in your download.
The MCP module that holds the dashboard's data as tools for the assistant.
Demo mode routes
Included with your purchase. Sign in to read, or open it in your download.
The routes a public demo uses: the demo switch and per-visitor accounts.
Custom payment gateways
Included with your purchase. Sign in to read, or open it in your download.
How the payment layer is built: the gateway and webhook interfaces, the simulator's webhook, and adding a processor.