Skip to the article
Aniq-UI

LearnioAPI reference

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:

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:

Terminal
curl http://localhost:8000/api/health
Response
{"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):

Terminal
curl -X POST http://localhost:8000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"not-an-email","password":"x"}'
Response (400)
{
  "success": false,
  "data": null,
  "message": "Enter a valid email address (example@domain.com).",
  "errors": {
    "email": ["Enter a valid email address (example@domain.com)."]
  }
}
StatusWhen
400A 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.
401The 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.
403The admin's roles do not grant the route's permission, or an instructor account reached outside its own courses.
404No such record.
409The write clashes with an existing record, such as an email or slug already in use.
429Too many attempts from one address on a sign-in, sign-up or password route. The Retry-After header says how many seconds to wait.
500An 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 listsAdmin lists
Pagepage, from 1page, from 1
Page sizepage_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.
SortCourses only: sort_by and sort_dir (asc or desc)sort and order (asc or desc). Orders use sort_by and sort_order.
Default orderCourses newest-updated first, blog newest-published firstNewest-updated first, ties broken by id. Categories and instructors keep their curated order.
Answerdata, current_page, last_page, per_page, total, from, to, plus page linksdata, 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 message of 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.

AudienceSign inToken in the answerAccepted on
StudentPOST /api/users/auth/logindata.token, with data.userStudent routes
AdminPOST /api/auth/logindata.access_token, with data.admin (roles and permissions)Admin routes
  • Both are JWTs signed with JWT_SECRET. Send them as Authorization: Bearer <token>.
  • A token lasts for JWT_EXPIRATION, 7d by default (Environment variables). The admin sign-in reports the same value as expires_in.
  • There is no refresh endpoint. When a token expires, the next request answers 401 and 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 401 with 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 429 with a Retry-After header. The count is kept in the API's memory, so it starts again on a restart and is counted separately by each instance.
  1. Sign in as the seeded Super Admin

    Terminal
    curl -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."
    }
  2. Call a protected route with the token

    Terminal
    curl "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.

  3. Sign in as the seeded student

    The sample data also holds a student, demo@learnio.com with the password Demo@123:

    Terminal
    curl -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.token the same way, for example to GET /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.

ModulePermissions
Adminsadmins.view, admins.create, admins.edit, admins.delete, admins.assign_roles
Rolesroles.view, roles.create, roles.edit, roles.delete, roles.assign_permissions
Settingssettings.view, settings.edit
Studentsstudents.view, students.create, students.update, students.delete, students.restore, students.verify
Categoriescategories.view, categories.create, categories.edit, categories.delete, categories.restore
Coursescourses.view, courses.create, courses.edit, courses.delete, courses.restore
Curriculumcurriculum.view, curriculum.create, curriculum.edit, curriculum.delete
Instructorsinstructors.view, instructors.create, instructors.edit, instructors.delete, instructors.restore
Enrolmentsenrollments.view, enrollments.create, enrollments.edit, enrollments.delete
Ordersorders.view, orders.create, orders.edit, orders.delete
Reviewsreviews.view, reviews.edit, reviews.delete, reviews.restore
Live classeslive_sessions.view, live_sessions.create, live_sessions.edit, live_sessions.delete
Assignmentsassignments.view, assignments.create, assignments.edit, assignments.delete
Blogblog.view, blog.create, blog.edit, blog.delete, blog.restore
AI assistantai_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:

RoleGrants
Super AdminEverything
AdminEverything except roles.*, admins.* and settings.edit
ManagerCourses, curriculum, categories, instructors, enrolments and orders without deletes, plus students.view, reviews.view and reviews.edit
Editorblog.*, reviews.view, reviews.edit, categories.view, courses.view, instructors.view
ViewerEvery .view permission
InstructorLive 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.

MethodPathWho can call itWhat it does
GET/api/healthAnyoneReadiness check
GET/api/users/coursesAnyoneLists the published public courses, paginated (page_count, the courses_per_page setting by default), with filters and sorting
GET/api/users/courses/categoriesAnyoneCategories that hold at least one public course, with their counts
GET/api/users/courses/:slugAnyoneOne 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/reviewsAnyoneThe course's published reviews, most recently updated first, paginated (page_count, 5 by default)
GET/api/users/InstructorsAnyoneInstructor directory, paginated (per_page, 8 by default), filter by specialty
GET/api/users/Instructors/:usernameAnyoneOne instructor with their public courses
GET/api/users/blogAnyonePublished posts, newest first, paginated (page_count, 9 by default), filter by tag
GET/api/users/blog/tagsAnyoneEvery tag in use, with how many posts carry it
GET/api/users/blog/:slugAnyoneOne published post
GET/api/users/platform/figuresAnyoneThe home page's counts: students, courses, instructors, countries and satisfaction
GET/api/categoriesAnyoneCategory list, paginated, with search, is_active, parent_id, has_courses
GET/api/categories/rootsAnyoneTop-level categories
GET/api/categories/slug/:slugAnyoneOne category by slug
GET/api/categories/:idAnyoneOne category by id
GET/api/helpers/countriesAnyoneCountries for a select field, in the request's language
GET/api/shop/payment-methodsAnyoneThe 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.

MethodPathWho can call itWhat it does
POST/api/users/auth/registerAnyoneCreates 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/loginAnyoneSigns in with email and password, answers token and user
POST/api/users/auth/logoutAnyoneNothing to revoke; lets the client clear its token
POST/api/users/auth/resendAnyoneMails the confirmation link again to email
POST/api/users/auth/verify-emailAnyoneConfirms the address with the token from the link (valid 24 hours)
POST/api/users/auth/forgot-passwordAnyoneMails a reset link to email (valid 1 hour)
POST/api/users/auth/reset-passwordAnyoneSets a new password with the token from the link
GET/api/users/auth/meStudentThe 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.

MethodPathWho can call itWhat it does
POST/api/students/auth/registerAnyoneCreates a student account and signs in, without a confirmation mail
POST/api/students/auth/loginAnyoneSigns in
GET/api/students/auth/meStudentThe signed-in student's profile
PATCH/api/students/auth/meStudentUpdates name, email or phone
PATCH/api/students/auth/me/passwordStudentChanges the password
POST/api/students/auth/forgot-passwordAnyoneMails a reset link to email, answering the same way whether or not the address has an account
POST/api/students/auth/reset-passwordAnyoneSets 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.

MethodPathWho can call itWhat it does
GET/api/users/profileStudentThe signed-in student
PATCH/api/users/profileStudentUpdates first_name, last_name, email or phone
PATCH/api/users/profile/passwordStudentChanges the password (the current one and a new one of 8 or more)
GET/api/users/dashboard/overviewStudentEverything the dashboard's home screen shows, in one call
GET/api/users/dashboard/kpisStudentThe student's headline figures
GET/api/users/dashboard/radarStudentThe skills chart, from the dashboard_radar_metrics setting
GET/api/users/enrollmentsStudentThe student's courses, paginated (page_count, 100 by default), filter by type, search, status
GET/api/users/enrollments/courses/:codeStudentA held course's full content, with each lesson's status and the progress
PATCH/api/users/enrollments/courses/:codeStudentRecords the last lesson opened (last_accessed_lesson_id)
POST/api/users/enrollments/lessons/:lessonCode/completeStudentMarks a lesson complete and updates the course progress
GET/api/users/enrollments/lessons/:lessonCode/noteStudentThe student's note on a lesson
PUT/api/users/enrollments/lessons/:lessonCode/noteStudentSaves the note (body)
GET/api/users/searchStudentGlobal search (q, limit per kind): the student's courses, the catalogue, instructors and more
GET/api/users/courses/:courseId/reviewStudentThe student's own review of the course, or null when there is none
PUT/api/users/courses/:courseId/reviewStudentWrites or replaces the student's review: rating from 1 to 5, optional comment up to 2,000 characters
DELETE/api/users/courses/:courseId/reviewStudentWithdraws 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 published straight away, or pending until a moderator approves it when the reviews_require_approval setting is true. Editing a review a moderator hid sends it back to pending.
  • A review a moderator deleted comes back with status: removed and 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

MethodPathWho can call itWhat it does
GET/api/users/quizzes?course_id=StudentQuizzes of a course the student holds
GET/api/users/quizzes/:idStudentOne quiz with its questions, without the answers
GET/api/users/quizzes/:id/attemptsStudentThe student's past attempts
POST/api/users/quizzes/:id/attemptsStudentSubmits answers (question id to chosen option ids) and returns the score
GET/api/users/dashboard/calendarStudentClasses and assignment deadlines between from and to (ISO dates, 7 days by default, 92 at most)
POST/api/users/dashboard/calendar/assignments/:id/submitStudentHands in an assignment (body)
DELETE/api/users/dashboard/calendar/assignments/:id/submitStudentWithdraws a submission

The calendar's live class routes are described with Live classes.

Student messages and notifications

MethodPathWho can call itWhat it does
GET/api/users/conversationsStudentThe student's conversations with their instructors
POST/api/users/conversationsStudentOpens, or returns, the conversation with the instructor of course_id
GET/api/users/conversations/:id/messagesStudentMessages in one conversation
POST/api/users/conversations/:id/messagesStudentSends a message: body, an image (attachment_url, attachment_name, attachment_type), or both
PATCH/api/users/conversations/:id/readStudentMarks the conversation read
PATCH/api/users/conversations/:id/unreadStudentMarks it unread
GET/api/users/notificationsStudentThe student's notifications
PATCH/api/users/notifications/:id/readStudentMarks one read
PATCH/api/users/notifications/read-allStudentMarks all read
DELETE/api/users/notifications/:idStudentDeletes 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

MethodPathWho can call itWhat it does
GET/api/shop/payment-methodsAnyonemethods the checkout offers and which are hosted
POST/api/shop/checkoutStudentBuys course_id, or up to 50 course_ids in one charge, with payment_method (card or paypal) and locale
POST/api/shop/orders/:reference/settleStudentFinishes a hosted payment when the buyer returns. Needed for PayPal; does nothing for Stripe.
GET/api/shop/ordersStudentThe student's orders
POST/api/webhooks/payments/stripeStripe, signedSettles, fails or refunds orders from Stripe's events
POST/api/webhooks/payments/paypalPayPal, signedSettles, 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

MethodPathWho can call itWhat it does
POST/api/auth/loginAnyoneSigns an admin in, answers access_token, roles and permissions. Rate limited.
GET/api/auth/meAny signed-in adminThe signed-in admin with roles and permissions
GET/api/adminsadmins.viewStaff list, filter by email, name, phone, role_id
GET/api/admins/statisticsadmins.viewStaff counts
GET/api/admins/:idadmins.viewOne admin
POST/api/adminsadmins.createCreates an admin
PATCH/api/admins/:idadmins.editUpdates an admin
PATCH/api/admins/:id/rolesadmins.assign_rolesReplaces the admin's roles (role_ids)
DELETE/api/admins/:idadmins.deleteDeletes an admin
PATCH/api/admins/profileAny signed-in adminUpdates the signed-in admin's own name, contact details and picture
PATCH/api/admins/profile/passwordAny signed-in adminChanges 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

MethodPathWho can call itWhat it does
GET/api/rolesroles.viewRoles, paginated when page is sent, filter by name, guard_name, created_from, created_to
GET/api/roles/statisticsroles.viewRole counts
GET/api/roles/selectroles.viewEvery role, for a picker
GET/api/roles/permissionsroles.viewEvery permission, grouped by module
GET/api/roles/:idroles.viewOne role with its permissions
POST/api/rolesroles.createCreates a role (name)
PUT/api/roles/:idroles.editRenames a role
POST/api/roles/:id/permissionsroles.assign_permissionsReplaces the role's permissions with permissions, a list of names
DELETE/api/roles/:idroles.deleteDeletes a role

Students

Student accounts are addressed by username. Deleting one is a soft delete that can be restored.

MethodPathWho can call itWhat it does
GET/api/usersstudents.viewStudents, with search, email, phone, country_id, username, first_name, last_name, from_date, to_date, verified
GET/api/users/statisticstudents.viewStudent counts
GET/api/users/:usernamestudents.viewOne student
POST/api/usersstudents.createCreates a student
PATCH/api/users/:usernamestudents.updateUpdates a student
PATCH/api/users/:username/change-passwordstudents.updateSets a student's password
POST/api/users/:username/resend-verification-emailstudents.updateMails 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-verifiedstudents.verifyMarks the email confirmed
POST/api/users/:username/make-unverifiedstudents.verifyMarks the email unconfirmed
DELETE/api/users/:usernamestudents.deleteMoves the student to the recycle bin
GET/api/users/deletedstudents.view, students.delete or students.restoreDeleted students
GET/api/users/deleted/:usernamestudents.view, students.delete or students.restoreOne deleted student
POST/api/users/deleted/:username/restorestudents.restoreRestores a deleted student

Instructors

MethodPathWho can call itWhat it does
GET/api/instructorsinstructors.viewInstructors, with search, specialty, status, is_featured
GET/api/instructors/statisticinstructors.viewInstructor counts
GET/api/instructors/username/:usernameinstructors.viewOne instructor by username
GET/api/instructors/:idinstructors.viewOne instructor
POST/api/instructorsinstructors.createCreates an instructor
PATCH/api/instructors/:idinstructors.editUpdates an instructor
DELETE/api/instructors/:idinstructors.deleteMoves an instructor to the recycle bin
GET/api/instructors/deletedinstructors.view, instructors.delete or instructors.restoreDeleted instructors
POST/api/instructors/deleted/:id/restoreinstructors.restoreRestores one

An instructor account messages its students through these routes, which answer 403 for an admin not linked to an instructor:

MethodPathWho can call itWhat it does
GET/api/conversationsInstructor accountThe instructor's conversations
GET/api/conversations/with/:userIdInstructor accountCourses the instructor shares with a student, to start a conversation from
POST/api/conversationsInstructor accountOpens, or returns, a conversation with user_id, optionally about course_id
GET/api/conversations/:id/messagesInstructor accountMessages in one conversation
POST/api/conversations/:id/messagesInstructor accountSends a message
PATCH/api/conversations/:id/readInstructor accountMarks it read
PATCH/api/conversations/:id/unreadInstructor accountMarks it unread

Categories

The reads are open to anyone and listed with the Public catalogue. Writes need a permission:

MethodPathWho can call itWhat it does
GET/api/categories/statisticcategories.viewCategory counts
POST/api/categoriescategories.createCreates a category
PATCH/api/categories/:idcategories.editUpdates a category
DELETE/api/categories/:idcategories.deleteMoves a category to the recycle bin
GET/api/categories/deletedcategories.delete or categories.restoreDeleted categories
POST/api/categories/deleted/:id/restorecategories.restoreRestores one

Courses

MethodPathWho can call itWhat it does
GET/api/coursescourses.viewCourses, with search, category_id, instructor_id, status, type, is_public, is_featured
GET/api/courses/statisticcourses.viewCourse counts
GET/api/courses/slug/:slugcourses.viewOne course by slug
GET/api/courses/:idcourses.viewOne course
POST/api/coursescourses.createCreates a course
PATCH/api/courses/:idcourses.editUpdates a course
DELETE/api/courses/:idcourses.deleteMoves a course to the recycle bin
GET/api/courses/deletedcourses.delete or courses.restoreDeleted courses
POST/api/courses/deleted/:id/restorecourses.restoreRestores one

Curriculum: sections, lessons and resources

MethodPathWho can call itWhat it does
GET/api/courses/:courseId/sectionscurriculum.viewA course's sections
POST/api/courses/:courseId/sectionscurriculum.createAdds a section
POST/api/courses/:courseId/sections/reordercurriculum.editSets the section order from ids
GET/api/sections/:idcurriculum.viewOne section
PATCH/api/sections/:idcurriculum.editUpdates a section
DELETE/api/sections/:idcurriculum.deleteDeletes a section
GET/api/sections/:sectionId/lessonscurriculum.viewA section's lessons
POST/api/sections/:sectionId/lessonscurriculum.createAdds a lesson
POST/api/sections/:sectionId/lessons/reordercurriculum.editSets the lesson order from ids
GET/api/lessons/:idcurriculum.viewOne lesson
PATCH/api/lessons/:idcurriculum.editUpdates a lesson
DELETE/api/lessons/:idcurriculum.deleteDeletes a lesson
GET/api/lessons/:lessonId/resourcescurriculum.viewA lesson's downloadable resources
POST/api/lessons/:lessonId/resourcescurriculum.editAttaches a resource
PATCH/api/lesson-resources/:idcurriculum.editUpdates a resource
DELETE/api/lesson-resources/:idcurriculum.editRemoves a resource

Quizzes and assignments

MethodPathWho can call itWhat it does
GET/api/quizzes?course_id=courses.viewA course's quizzes (course_id is required)
GET/api/quizzes/:idcourses.viewOne quiz with its questions and answers
POST/api/quizzescourses.createCreates a quiz
PATCH/api/quizzes/:idcourses.editUpdates a quiz
DELETE/api/quizzes/:idcourses.deleteDeletes a quiz
POST/api/quizzes/:id/questionscourses.editAdds a question
PATCH/api/quizzes/:id/questions/:questionIdcourses.editUpdates a question
DELETE/api/quizzes/:id/questions/:questionIdcourses.editDeletes a question
GET/api/assignmentsassignments.viewAssignments, with search, course_id, lesson_id, status, from, to
GET/api/assignments/:idassignments.viewOne assignment
GET/api/assignments/:id/submissionsassignments.viewThe submissions waiting to be marked
PATCH/api/assignments/submissions/:submissionId/gradeassignments.editMarks a submission
POST/api/assignmentsassignments.createCreates an assignment
PATCH/api/assignments/:idassignments.editUpdates an assignment
DELETE/api/assignments/:idassignments.deleteDeletes an assignment

Enrolments and orders

MethodPathWho can call itWhat it does
GET/api/enrollmentsenrollments.viewEnrolments, with search, course_id, user_id, status, date_from, date_to
GET/api/enrollments/statisticenrollments.viewEnrolment counts
GET/api/enrollments/:idenrollments.viewOne enrolment
POST/api/enrollmentsenrollments.createEnrols a student by hand
PATCH/api/enrollments/:idenrollments.editUpdates an enrolment
DELETE/api/enrollments/:idenrollments.deleteRemoves an enrolment
GET/api/ordersorders.viewOrders, with search, status, payment_method, course_id, user_id, date_from, date_to, page, limit, sort_by, sort_order
GET/api/orders/revenueorders.viewRevenue between date_from and date_to. The average order value leaves out free orders.
GET/api/orders/revenue/seriesorders.viewDaily revenue for the last days (30 by default, 365 at most)
GET/api/orders/:idorders.viewOne order
POST/api/ordersorders.createRecords a seat paid for elsewhere
PATCH/api/orders/:idorders.editUpdates an order
DELETE/api/orders/:idorders.deleteDeletes 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

MethodPathWho can call itWhat it does
GET/api/course-reviewsreviews.viewReviews, with search, course_id, user_id, rating, status
GET/api/course-reviews/statisticreviews.viewReview counts
GET/api/course-reviews/:idreviews.viewOne review
PATCH/api/course-reviews/:idreviews.editModerates a review: status is published, pending or hidden
DELETE/api/course-reviews/:idreviews.deleteMoves a review to the recycle bin
GET/api/course-reviews/deletedreviews.delete or reviews.restoreDeleted reviews
POST/api/course-reviews/deleted/:id/restorereviews.restoreRestores 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

MethodPathWho can call itWhat it does
GET/api/blogblog.viewPosts in every status, with search, category_slug, status, tag
GET/api/blog/statisticblog.viewPost counts
GET/api/blog/slug/:slugblog.viewOne post by slug
GET/api/blog/:idblog.viewOne post
POST/api/blogblog.createCreates a post
PATCH/api/blog/:idblog.editUpdates a post
DELETE/api/blog/:idblog.deleteMoves a post to the recycle bin
GET/api/blog/deletedblog.delete or blog.restoreDeleted posts
POST/api/blog/deleted/:id/restoreblog.restoreRestores one

Settings

Platform settings are stored as key and value pairs and addressed by key.

MethodPathWho can call itWhat it does
GET/api/settingssettings.viewSettings, with search, category, type
GET/api/settings/:keysettings.viewOne setting
PATCH/api/settings/:keysettings.editChanges a setting's value
DELETE/api/settings/:keysettings.editDeletes a setting

The seeder creates these six, and each is read by the API or the student dashboard:

KeySeeded valueWhat it does
site_nameLearnioThe product name written into every email the API sends
default_localeenThe language used when a request names no supported language. A value that is not a supported language is refused with 400.
support_emailsupport@learnio.comThe reply-to address of every email, so a reply reaches a person
courses_per_page12The public catalogue's page size when the request sets none (1 to 100)
reviews_require_approvalfalseWhen true, a student's review waits as pending until a moderator publishes it
dashboard_radar_metricsA JSON map of metric to scoreThe 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

MethodPathWho can call itWhat it does
POST/api/helpers/uploadAny signed-in admin, or a studentUploads 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.

Terminal
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

MethodPathWho can call itWhat it does
GET/api/notificationsAny signed-in adminThe signed-in admin's notifications and unread count
PATCH/api/notifications/read-allAny signed-in adminMarks all read, answers the whole feed
PATCH/api/notifications/:id/readAny signed-in adminMarks one read, answers the whole feed
DELETE/api/notifications/:idAny signed-in adminDeletes one, answers the whole feed
GET/api/searchAny signed-in adminGlobal search (q, limit per kind). Only the kinds the admin's permissions allow are searched.
GET/api/statistics/trendscourses.viewThe figures behind every stat card, in one call
GET/api/statistics/overviewcourses.viewThe 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:

TypeWhenWho receives it
enrollment_createdA student is given a seat, by a purchase, a free course or staffenrollments.view
course_completedA student finishes a courseenrollments.view
course_review_submittedA student leaves a review, or edits one that waits for approvalreviews.view
course_publishedA course is publishedcourses.view
student_registeredA student creates an accountstudents.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.

Stuck on a step?

Find a fix before you start over.

Troubleshooting

Cookie Preferences

We use cookies to enhance your browsing experience, analyze site traffic, and personalize content. By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising.