Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Moodle

Integrate Bolt.new with Moodle by enabling Moodle's Web Services, creating an external service and token in Site Administration, then calling Moodle's function-based REST API from Next.js API routes using the wsfunction parameter pattern. Moodle's API differs from standard REST — every call specifies a function name like core_course_get_courses. Store your token in .env and never expose it client-side.

What you'll learn

  • How to enable Moodle Web Services and create an API token in Site Administration
  • How Moodle's function-based API differs from standard REST and how to call it correctly
  • How to fetch courses, enrolled students, assignments, and grades via Next.js API routes
  • How to build a custom student dashboard that surfaces Moodle data outside the LMS interface
  • How to handle Moodle's response format including exception objects and paginated results
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate18 min read35 minutesOtherApril 2026RapidDev Engineering Team
TL;DR

Integrate Bolt.new with Moodle by enabling Moodle's Web Services, creating an external service and token in Site Administration, then calling Moodle's function-based REST API from Next.js API routes using the wsfunction parameter pattern. Moodle's API differs from standard REST — every call specifies a function name like core_course_get_courses. Store your token in .env and never expose it client-side.

Building Custom Learning Dashboards with Moodle's Web Services API

Moodle is different from every other API you will encounter in Bolt.new development. Rather than resource-based URLs (GET /courses, POST /enrollments), Moodle exposes all its functionality through a single endpoint that accepts a function name as a parameter. Want courses? Call wsfunction=core_course_get_courses. Want assignments? Call wsfunction=mod_assign_get_assignments. This design, inherited from Moodle's PHP architecture, means every integration follows the same pattern regardless of what data you are accessing — a single fetch to the same URL, varying only the function name and parameters.

This architecture has a major practical benefit for Bolt developers: once you understand the pattern, adding new Moodle capabilities to your app is fast. The Moodle Web Services documentation lists over 200 available functions covering every aspect of the LMS — courses, users, grades, assignments, quizzes, forums, and messaging. You can build a custom student portal, a faculty gradebook, a corporate training tracker, or an executive dashboard showing completion rates, all from the same API foundation.

The setup requires Moodle administrator access. A Moodle developer (with Site Administration access) needs to enable Web Services, create an external service listing the specific functions your app needs, create a user account for the integration, and generate a token. Once you have the token and your Moodle site's URL, the integration is straightforward: API calls go through Next.js API routes to keep the token server-side, the Moodle endpoint returns JSON, and your React components display the data. Moodle's Web Services work over standard HTTPS, which is fully compatible with Bolt's WebContainer for outbound calls.

Integration method

Bolt Chat + API Route

Bolt generates the Moodle integration code — API routes that call Moodle's Web Services REST endpoint — through conversation with the AI. Unlike modern REST APIs, Moodle uses a function-based model where every call specifies a wsfunction parameter (like core_course_get_courses) against a single endpoint. Your Moodle administrator must enable Web Services, create an external service with the required functions, and generate a token. All API calls go through Next.js server-side routes to keep the token out of the browser.

Prerequisites

  • Moodle Site Administration access (you need admin credentials to enable Web Services — student/teacher accounts cannot do this)
  • A Moodle instance accessible over HTTPS (self-hosted or Moodle.com — version 3.9 or later recommended)
  • Web Services enabled in Moodle: Site Administration → Server → Web Services → Overview → Enable web services
  • An external service created in Moodle with the specific wsfunction names your app will call
  • A Moodle Web Services token generated for your integration user (Site Administration → Server → Web Services → Manage tokens)

Step-by-step guide

1

Enable Moodle Web Services and generate an API token

Before writing any code, your Moodle instance must have Web Services enabled and configured. This requires Site Administration access — you cannot set this up as a regular user. Log into your Moodle site as an administrator and navigate through the steps carefully. Go to Site Administration → Server → Web Services → Overview. This page shows a checklist of required setup steps. Work through them in order: Enable Web Services (toggle it on), Enable Protocols (enable the REST protocol), and Create a service (create a new External Service that lists exactly which API functions your app will use — only add the functions you need, not all functions). For a basic student dashboard, add these functions to your external service: core_course_get_courses, core_enrol_get_users_courses, mod_assign_get_assignments, gradereport_user_get_grade_items, core_user_get_users_by_field. For a more complete integration, add: core_completion_get_activities_completion_status, mod_assign_get_submissions, core_enrol_get_enrolled_users. Next, create or designate a Moodle user account for your integration (ideally a dedicated service account, not an admin account). Assign this user the 'webservice' role or ensure it has the capabilities required by the functions you added. Then go to Site Administration → Server → Web Services → Manage Tokens, click Add, select your service and user, and generate a token. Copy this token — it is a long string and will be your primary authentication credential. Store the token in your Bolt project's .env.local file. Also store your Moodle site URL. These are the only two configuration values needed to call the API.

Bolt.new Prompt

Create a Next.js app that integrates with a Moodle LMS Web Services API. Create a utility file lib/moodle.ts that exports a moodleCall function. This function takes a wsfunction name and parameters object, calls the Moodle REST endpoint at {MOODLE_URL}/webservice/rest/server.php, passes the wstoken, moodlewsrestformat=json, and the wsfunction and other parameters as query parameters for GET requests. Handle Moodle's exception response format — if the response has an 'exception' field, throw an error with the message field. Store MOODLE_URL and MOODLE_TOKEN in .env.

Paste this in Bolt.new chat

lib/moodle.ts
1// .env.local
2MOODLE_URL=https://your-moodle-site.com
3MOODLE_TOKEN=your_web_services_token_here
4
5// lib/moodle.ts
6const MOODLE_URL = process.env.MOODLE_URL;
7const MOODLE_TOKEN = process.env.MOODLE_TOKEN;
8
9interface MoodleException {
10 exception: string;
11 errorcode: string;
12 message: string;
13 debuginfo?: string;
14}
15
16export async function moodleCall<T>(
17 wsfunction: string,
18 params: Record<string, string | number | boolean> = {}
19): Promise<T> {
20 if (!MOODLE_URL || !MOODLE_TOKEN) {
21 throw new Error('MOODLE_URL and MOODLE_TOKEN must be set in environment variables');
22 }
23
24 const url = new URL(`${MOODLE_URL}/webservice/rest/server.php`);
25 url.searchParams.set('wstoken', MOODLE_TOKEN);
26 url.searchParams.set('moodlewsrestformat', 'json');
27 url.searchParams.set('wsfunction', wsfunction);
28
29 Object.entries(params).forEach(([key, value]) => {
30 url.searchParams.set(key, String(value));
31 });
32
33 const response = await fetch(url.toString(), {
34 headers: { 'Content-Type': 'application/json' },
35 });
36
37 if (!response.ok) {
38 throw new Error(`Moodle API HTTP error: ${response.status}`);
39 }
40
41 const data = await response.json() as T | MoodleException;
42
43 // Moodle returns exceptions as JSON with an 'exception' field
44 if (data && typeof data === 'object' && 'exception' in data) {
45 const err = data as MoodleException;
46 throw new Error(`Moodle API error: ${err.message} (${err.errorcode})`);
47 }
48
49 return data as T;
50}

Pro tip: Moodle's Web Services REST endpoint always uses GET for reading data, even for operations that feel like POST (such as grade submissions — those use POST with form-encoded body). The moodleCall utility above handles GET requests. For write operations, add a separate POST-capable variant that sends parameters in the request body.

Expected result: Your .env.local has MOODLE_URL and MOODLE_TOKEN set. The moodleCall utility is ready to use in API routes. Testing with a simple call like core_site_get_site_info returns your Moodle site name and version, confirming the token is valid.

2

Fetch courses and enrollments from the Moodle API

With the moodleCall utility in place, you can now create Next.js API routes that retrieve course and enrollment data. These routes act as a proxy between your React frontend and Moodle — they receive requests from the browser, call Moodle using the server-side token, and return normalized JSON to the client. The most common starting point is fetching a user's enrolled courses. The function core_enrol_get_users_courses returns the courses that a specific Moodle user is enrolled in, along with progress, last access time, and course metadata. It requires the user's Moodle ID as a parameter — not a username or email. To look up a user's Moodle ID by email, use core_user_get_users_by_field first. For a course listing dashboard, you will typically want: the course ID, full name, short name, course image URL, category, start and end dates, progress percentage, and last access timestamp. The core_enrol_get_users_courses function returns most of these in its response. Map the raw Moodle response to a clean interface that your React components expect — this normalization layer makes your frontend code much cleaner and protects it from Moodle API changes. Note on Moodle's response format: arrays of items come back as JSON arrays, but individual items with fields like course metadata come back as objects. Some functions return a nested structure with course data inside a courses key, while others return the array directly. Check the Moodle Web Services documentation for each function to understand its specific response shape.

Bolt.new Prompt

Create API routes for Moodle data. First, create /api/moodle/user/[email]/route.ts that calls core_user_get_users_by_field to look up a Moodle user ID by email address. Second, create /api/moodle/courses/[userId]/route.ts that calls core_enrol_get_users_courses with the user ID and returns a normalized array with id, fullname, shortname, progress, lastaccess (formatted date), imageUrl, and category. Use the moodleCall utility from lib/moodle.ts.

Paste this in Bolt.new chat

app/api/moodle/courses/[userId]/route.ts
1// app/api/moodle/courses/[userId]/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { moodleCall } from '@/lib/moodle';
4
5interface MoodleCourse {
6 id: number;
7 fullname: string;
8 shortname: string;
9 overviewfiles?: Array<{ fileurl: string }>;
10 progress?: number;
11 lastaccess?: number;
12 categoryname?: string;
13 startdate?: number;
14 enddate?: number;
15 visible?: number;
16}
17
18export async function GET(
19 request: NextRequest,
20 { params }: { params: { userId: string } }
21) {
22 const userId = parseInt(params.userId, 10);
23
24 if (isNaN(userId)) {
25 return NextResponse.json({ error: 'Invalid user ID' }, { status: 400 });
26 }
27
28 try {
29 const courses = await moodleCall<MoodleCourse[]>(
30 'core_enrol_get_users_courses',
31 { userid: userId }
32 );
33
34 const normalized = courses.map((course) => ({
35 id: course.id,
36 fullname: course.fullname,
37 shortname: course.shortname,
38 progress: course.progress ?? 0,
39 lastAccess: course.lastaccess
40 ? new Date(course.lastaccess * 1000).toISOString()
41 : null,
42 imageUrl: course.overviewfiles?.[0]?.fileurl
43 ? `${course.overviewfiles[0].fileurl}?token=${process.env.MOODLE_TOKEN}`
44 : null,
45 category: course.categoryname ?? 'Uncategorized',
46 startDate: course.startdate ? new Date(course.startdate * 1000).toISOString() : null,
47 endDate: course.enddate ? new Date(course.enddate * 1000).toISOString() : null,
48 isVisible: course.visible === 1,
49 }));
50
51 return NextResponse.json({ courses: normalized, total: normalized.length });
52 } catch (error) {
53 const message = error instanceof Error ? error.message : 'Unknown error';
54 return NextResponse.json({ error: message }, { status: 500 });
55 }
56}

Pro tip: Moodle stores timestamps as Unix timestamps (seconds since epoch), not milliseconds. Multiply by 1000 before passing to JavaScript's Date constructor. Also, Moodle course images require the API token appended as a query parameter to be accessible — this is handled in the imageUrl mapping above.

Expected result: Calling /api/moodle/courses/123 (with a valid Moodle user ID) returns a JSON array of courses that user is enrolled in, with progress percentages and last access dates.

3

Fetch assignments and grades for student progress tracking

Beyond courses, the most valuable data for a student dashboard is assignment deadlines and current grades. Moodle exposes these through separate Web Service functions — mod_assign_get_assignments for assignment data and gradereport_user_get_grade_items for the grade report. The mod_assign_get_assignments function returns all assignments across one or more course IDs. Pass course IDs as an array using Moodle's indexed parameter convention: courseids[0]=101&courseids[1]=102. Each assignment in the response includes the name, due date (Unix timestamp), cut-off date, description, and submission status information. For grades, gradereport_user_get_grade_items returns the complete gradebook for a user in a specific course — similar to what you see in the Moodle gradebook view. The response includes each grade item's name, grade value, maximum grade, feedback, and percentage. This is the most useful endpoint for showing a student their overall standing in each course. Be aware of a performance consideration: fetching assignments across many courses in a single call can be slow if the Moodle server is under load. For a student enrolled in 10+ courses, consider fetching course data first, then loading assignments on-demand when a user clicks into a specific course, rather than loading all assignments upfront. This lazy loading pattern keeps the initial dashboard fast. Also note that Moodle's Web Services do not push real-time updates — your app must poll for new data. For a dashboard refreshed on page load, this is fine. For real-time grade updates, Moodle does not provide a native webhook or event stream — the standard approach is polling on a schedule.

Bolt.new Prompt

Create two more Moodle API routes. First: /api/moodle/assignments/[courseId]/route.ts that calls mod_assign_get_assignments with courseids[0]={courseId} and returns assignments with id, name, dueDate (ISO string), cutoffDate, description, and submissionsOpen (boolean based on whether duedate is in the future). Second: /api/moodle/grades/[courseId]/[userId]/route.ts that calls gradereport_user_get_grade_items and returns grade items with itemName, grade, gradeMax, percentage, and feedback.

Paste this in Bolt.new chat

app/api/moodle/assignments/[courseId]/route.ts
1// app/api/moodle/assignments/[courseId]/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { moodleCall } from '@/lib/moodle';
4
5interface MoodleAssignment {
6 id: number;
7 cmid: number;
8 name: string;
9 duedate: number;
10 cutoffdate: number;
11 intro: string;
12 nosubmissions: number;
13}
14
15interface AssignmentsResponse {
16 courses: Array<{
17 id: number;
18 assignments: MoodleAssignment[];
19 }>;
20}
21
22export async function GET(
23 request: NextRequest,
24 { params }: { params: { courseId: string } }
25) {
26 const courseId = parseInt(params.courseId, 10);
27
28 if (isNaN(courseId)) {
29 return NextResponse.json({ error: 'Invalid course ID' }, { status: 400 });
30 }
31
32 try {
33 const data = await moodleCall<AssignmentsResponse>(
34 'mod_assign_get_assignments',
35 { 'courseids[0]': courseId }
36 );
37
38 const now = Date.now();
39 const assignments = (data.courses[0]?.assignments ?? []).map((a) => ({
40 id: a.id,
41 name: a.name,
42 dueDate: a.duedate ? new Date(a.duedate * 1000).toISOString() : null,
43 cutoffDate: a.cutoffdate ? new Date(a.cutoffdate * 1000).toISOString() : null,
44 description: a.intro.replace(/<[^>]*>/g, ''), // Strip HTML from intro
45 submissionsOpen: a.nosubmissions === 0 && (a.duedate === 0 || a.duedate * 1000 > now),
46 isOverdue: a.duedate > 0 && a.duedate * 1000 < now,
47 }));
48
49 return NextResponse.json({
50 courseId,
51 assignments,
52 total: assignments.length,
53 });
54 } catch (error) {
55 const message = error instanceof Error ? error.message : 'Unknown error';
56 return NextResponse.json({ error: message }, { status: 500 });
57 }
58}

Pro tip: Moodle assignment descriptions contain HTML markup (it is a rich text field in Moodle). Strip HTML tags before displaying in your React components, or use dangerouslySetInnerHTML with DOMPurify sanitization for formatted display. Never render unsanitized Moodle HTML directly.

Expected result: Calling /api/moodle/assignments/456 returns a list of assignments for course 456 with due dates formatted as ISO strings and an isOverdue flag for assignments past their deadline.

4

Build a student dashboard and handle Moodle errors

With the API routes in place, build the React dashboard UI. The student dashboard should show enrolled courses as cards with progress indicators, upcoming assignment deadlines sorted by due date, and current grades for selected courses. This gives students everything they need without navigating Moodle's complex default interface. The main dashboard page should prompt the user for their Moodle user ID (or derive it from an authenticated session if you have one) and fetch their courses on load. Each course card shows the course name, progress percentage, last access time, and a click to expand assignments and grades for that course. For the assignment list, filter to show only future assignments (duedate in the future) by default, with an option to show past assignments. Sort by due date ascending so the most urgent assignments appear first. Highlight assignments due within 48 hours in yellow, overdue in red. Remember that Bolt's WebContainer handles outbound API calls fine — your dashboard can call /api/moodle/courses and /api/moodle/assignments during development without deploying. The only Moodle integration feature that requires deployment would be any webhook or scheduled sync, and Moodle's Web Services do not natively support outgoing webhooks (Moodle has an Events API for plugins, but not a standard HTTP webhook for external apps). For real-time data in a production deployment, implement polling with SWR or React Query.

Bolt.new Prompt

Build a MoodleDashboard React component. It should prompt for a Moodle user ID, fetch their courses from /api/moodle/courses/[userId], and display them as cards in a responsive grid. Each card shows the course name, progress bar (0-100%), last access date, and category. Clicking a course card expands it to show assignments fetched from /api/moodle/assignments/[courseId] sorted by due date. Highlight overdue assignments in red and due-within-48-hours in yellow. Show a loading skeleton while fetching. Add error handling for invalid user IDs.

Paste this in Bolt.new chat

components/MoodleDashboard.tsx
1// components/MoodleDashboard.tsx
2'use client';
3
4import { useState } from 'react';
5
6interface Course {
7 id: number;
8 fullname: string;
9 shortname: string;
10 progress: number;
11 lastAccess: string | null;
12 category: string;
13 imageUrl: string | null;
14}
15
16interface Assignment {
17 id: number;
18 name: string;
19 dueDate: string | null;
20 isOverdue: boolean;
21 submissionsOpen: boolean;
22 description: string;
23}
24
25function urgencyClass(assignment: Assignment): string {
26 if (assignment.isOverdue) return 'border-l-4 border-red-500 bg-red-50';
27 if (assignment.dueDate) {
28 const hoursLeft = (new Date(assignment.dueDate).getTime() - Date.now()) / 3600000;
29 if (hoursLeft < 48) return 'border-l-4 border-yellow-400 bg-yellow-50';
30 }
31 return 'border-l-4 border-gray-200';
32}
33
34export default function MoodleDashboard() {
35 const [userId, setUserId] = useState('');
36 const [courses, setCourses] = useState<Course[]>([]);
37 const [assignments, setAssignments] = useState<Record<number, Assignment[]>>({});
38 const [loading, setLoading] = useState(false);
39 const [error, setError] = useState('');
40 const [expandedCourse, setExpandedCourse] = useState<number | null>(null);
41
42 async function fetchCourses() {
43 if (!userId) return;
44 setLoading(true);
45 setError('');
46 try {
47 const res = await fetch(`/api/moodle/courses/${userId}`);
48 const data = await res.json();
49 if (data.error) throw new Error(data.error);
50 setCourses(data.courses);
51 } catch (e) {
52 setError(e instanceof Error ? e.message : 'Failed to load courses');
53 } finally {
54 setLoading(false);
55 }
56 }
57
58 async function loadAssignments(courseId: number) {
59 if (assignments[courseId]) {
60 setExpandedCourse(expandedCourse === courseId ? null : courseId);
61 return;
62 }
63 const res = await fetch(`/api/moodle/assignments/${courseId}`);
64 const data = await res.json();
65 setAssignments((prev) => ({ ...prev, [courseId]: data.assignments ?? [] }));
66 setExpandedCourse(courseId);
67 }
68
69 return (
70 <div className="p-6 max-w-4xl mx-auto">
71 <h1 className="text-2xl font-bold mb-6">Moodle Student Dashboard</h1>
72 <div className="flex gap-3 mb-8">
73 <input
74 type="number"
75 value={userId}
76 onChange={(e) => setUserId(e.target.value)}
77 placeholder="Moodle User ID"
78 className="border rounded px-3 py-2 w-48"
79 />
80 <button
81 onClick={fetchCourses}
82 disabled={loading}
83 className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
84 >
85 {loading ? 'Loading...' : 'Load Courses'}
86 </button>
87 </div>
88 {error && <p className="text-red-600 mb-4">{error}</p>}
89 <div className="grid gap-4">
90 {courses.map((course) => (
91 <div key={course.id} className="border rounded-lg overflow-hidden">
92 <button
93 onClick={() => loadAssignments(course.id)}
94 className="w-full p-4 text-left hover:bg-gray-50"
95 >
96 <div className="flex justify-between items-start">
97 <div>
98 <h2 className="font-semibold">{course.fullname}</h2>
99 <p className="text-sm text-gray-500">{course.category}</p>
100 </div>
101 <span className="text-sm text-blue-600">{course.progress}% complete</span>
102 </div>
103 <div className="mt-2 h-2 bg-gray-200 rounded">
104 <div
105 className="h-2 bg-blue-500 rounded"
106 style={{ width: `${course.progress}%` }}
107 />
108 </div>
109 </button>
110 {expandedCourse === course.id && assignments[course.id] && (
111 <div className="border-t p-4 bg-gray-50">
112 <h3 className="font-medium mb-3">Assignments</h3>
113 {assignments[course.id].length === 0 ? (
114 <p className="text-sm text-gray-500">No assignments.</p>
115 ) : (
116 assignments[course.id].map((a) => (
117 <div key={a.id} className={`mb-2 p-3 rounded ${urgencyClass(a)}`}>
118 <p className="font-medium text-sm">{a.name}</p>
119 <p className="text-xs text-gray-500">
120 {a.dueDate
121 ? `Due: ${new Date(a.dueDate).toLocaleDateString()}`
122 : 'No due date'}
123 {a.isOverdue && ' — OVERDUE'}
124 </p>
125 </div>
126 ))
127 )}
128 </div>
129 )}
130 </div>
131 ))}
132 </div>
133 </div>
134 );
135}

Pro tip: For production deployments, consider implementing a simple caching layer (using Next.js revalidation or an in-memory cache) for course data. Moodle course enrollment does not change frequently, so caching for 5-10 minutes significantly reduces load on your Moodle server.

Expected result: Entering a Moodle user ID and clicking Load Courses shows that user's enrolled courses as expandable cards. Clicking a course reveals its assignments, with overdue items highlighted in red and upcoming-deadline items in yellow.

Common use cases

Custom student progress dashboard

Build a simplified student-facing portal that shows enrolled courses, assignment due dates, and current grades in a cleaner interface than Moodle's default theme. Students see their academic status at a glance without navigating Moodle's complex menu structure.

Bolt.new Prompt

Create a Next.js app connected to a Moodle LMS. Build a student dashboard that fetches enrolled courses for a specific user ID using core_enrol_get_users_courses, shows assignment due dates using mod_assign_get_assignments, and displays current grades. Use the Moodle Web Services REST API with a token from .env. Show courses as cards with progress indicators.

Copy this prompt to try it in Bolt.new

Corporate training completion tracker

Build an internal HR dashboard showing which employees have completed mandatory training courses in Moodle. HR managers can filter by department, see completion percentages, and identify employees who have not started required courses.

Bolt.new Prompt

Build a training completion dashboard that calls Moodle's Web Services API. Fetch all users enrolled in a specific course using core_enrol_get_enrolled_users, get their activity completion data using core_completion_get_activities_completion_status, and display a table showing each employee's name, department, completion percentage, and last access date. Add a filter for incomplete-only view.

Copy this prompt to try it in Bolt.new

Faculty grade submission tool

Create a streamlined grading interface for instructors that pulls assignment submissions from Moodle, lets faculty enter grades in a simple form, and submits them back to Moodle via the API — all without navigating Moodle's complex grading workflows.

Bolt.new Prompt

Create a faculty grading interface that fetches assignment submissions from Moodle using mod_assign_get_submissions and mod_assign_get_submission_statuses. Display each student's submission with a text input for the grade (0-100). On save, call mod_assign_save_grade to submit grades back to Moodle. Add bulk grade entry and a preview before submitting.

Copy this prompt to try it in Bolt.new

Troubleshooting

Moodle API returns an exception object with errorcode 'invalidtoken' instead of data

Cause: The MOODLE_TOKEN environment variable is incorrect, the token was revoked in Moodle's Web Services management, or the token belongs to a different Moodle site than MOODLE_URL points to.

Solution: Log into your Moodle site as an administrator and go to Site Administration → Server → Web Services → Manage Tokens to confirm the token is active and not expired. Copy the token again carefully — it is a 32-character alphanumeric string with no spaces. Set it in your .env.local file and restart the Next.js dev server. If the token is missing from the Manage Tokens page, generate a new one.

typescript
1// Test token validity with the simplest possible function:
2const result = await moodleCall('core_webservice_get_site_info');
3console.log('Site:', result.sitename, 'Moodle version:', result.release);

API returns 'accessexception' or 'nopermission' error for specific wsfunction calls

Cause: The function is not added to the External Service that your token belongs to, or the Moodle user account the token represents does not have the capability required to call that function.

Solution: In Moodle Site Administration → Server → Web Services → External Services, open your service and check that the specific wsfunction is listed under 'Functions'. If it is missing, add it. Also check that the user assigned to the token has the required Moodle role capabilities — teacher-level functions require at minimum a teacher role in the relevant courses.

fetch to Moodle URL fails with CORS error in the browser console during development

Cause: The Moodle API call is being made from client-side React code directly, not through a Next.js API route. Moodle does not send CORS headers that allow browser-origin requests.

Solution: Move all Moodle API calls to server-side Next.js API routes in the app/api/ directory. Client components should fetch from your own API routes (e.g., /api/moodle/courses/123), not directly from the Moodle URL. Server-side fetch calls are not subject to CORS restrictions.

typescript
1// WRONG — direct Moodle call from client component:
2const data = await fetch('https://moodle.school.edu/webservice/rest/server.php?...');
3
4// CORRECT — call your own API route from client:
5const data = await fetch('/api/moodle/courses/123');

Course images do not load — broken image icons appear instead of course thumbnails

Cause: Moodle course overview images require the Web Services token appended as a query parameter to authenticate the file request. Without the token, Moodle returns a 403 Unauthorized response.

Solution: Append ?token={MOODLE_TOKEN} to the fileurl from overviewfiles. This is handled in the API route above. However, if you are passing the imageUrl directly to an <img> src in the browser, the token is visible in the page HTML. For security, proxy image requests through your own API route that fetches and streams the image without exposing the token to the client.

typescript
1// Safe approach: proxy image through your API route
2// app/api/moodle/image/route.ts — fetch and stream the Moodle image server-side
3export async function GET(request: NextRequest) {
4 const { searchParams } = new URL(request.url);
5 const fileUrl = searchParams.get('url');
6 const response = await fetch(`${fileUrl}?token=${process.env.MOODLE_TOKEN}`);
7 return new NextResponse(response.body, {
8 headers: { 'Content-Type': response.headers.get('Content-Type') ?? 'image/jpeg' },
9 });
10}

Best practices

  • Create a dedicated Moodle service account for your integration rather than using an admin account — this limits what the API token can access and makes it easier to audit API usage.
  • Only add the specific wsfunction names your app needs to the External Service, not all available functions. This is a security principle of least privilege — if the token is compromised, attackers can only call the functions you explicitly enabled.
  • Never call Moodle's Web Services endpoint directly from client-side React. Always proxy through Next.js API routes to keep your token server-side and avoid CORS errors.
  • Strip HTML from Moodle text fields (course descriptions, assignment intros) before displaying them — Moodle stores these as rich HTML. Use a simple regex for display text or DOMPurify for formatted display.
  • Cache course and enrollment data that does not change frequently. A 5-minute cache significantly reduces load on your Moodle server for dashboards that many students access simultaneously.
  • Always deploy to Netlify or Bolt Cloud before testing in a real school environment — while Moodle API calls work in the WebContainer preview, real student data should only flow through a secured, deployed application.
  • Handle Moodle's Unix timestamp format (seconds, not milliseconds) by multiplying by 1000 before passing to JavaScript Date. Missing this conversion produces dates in 1970.

Alternatives

Frequently asked questions

Do I need to self-host Moodle to use the Web Services API with Bolt.new?

No. Both self-hosted Moodle instances and Moodle.com (the managed cloud service) support Web Services. You just need administrator access to enable Web Services and create a token. Contact your Moodle administrator if you do not have Site Administration access.

Can I test the Moodle integration in Bolt's WebContainer preview?

Yes. Moodle API calls from your Next.js API routes are outbound HTTP requests, which work fine in Bolt's WebContainer. You can fully test fetching courses, assignments, and grades in the preview without deploying. The only thing that requires deployment is any feature that needs to receive incoming HTTP requests, which Moodle's standard Web Services do not use.

How do I look up a Moodle user ID from an email address?

Use the core_user_get_users_by_field function with field=email and values[0]=user@example.com. This returns the user object including their Moodle numeric ID. In a production app, look up and cache the Moodle user ID during authentication so you do not need to call this endpoint on every page load.

Is there a way to write data back to Moodle — for example, submitting grades from my Bolt app?

Yes. Functions like mod_assign_save_grade and gradereport_grader_get_grades allow writing grades back to Moodle. These functions use POST requests with parameters in the request body. The user account associated with your token must have grading permissions in the relevant course — a teacher or grader role. Write operations should be used carefully and always validated before submission.

What Moodle version do I need for Web Services to work?

Moodle Web Services have been available since Moodle 2.0 (2010). The functions used in this guide (core_course_get_courses, mod_assign_get_assignments, gradereport_user_get_grade_items) are stable and available in all Moodle versions from 3.x onwards. Moodle 3.9 LTS and Moodle 4.x are recommended for current integrations.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.