# Moodle

- Tool: Bubble
- Difficulty: Intermediate
- Time required: 3–5 hours
- Last updated: July 2026

## TL;DR

Moodle's API is unlike standard REST APIs: every function call hits the same single endpoint and the operation is determined by a 'wsfunction' URL parameter. In Bubble's API Connector, set the base URL to your Moodle site's /webservice/rest/server.php, mark the wstoken as Private, always include moodlewsrestformat=json, and add separate API calls per function. The critical gotcha: Moodle returns HTTP 200 even on authentication errors — check the JSON body for an 'exception' field in every workflow.

## Moodle's one-endpoint API: what makes it unique in Bubble

When Bubble developers first encounter Moodle's API documentation, it looks unfamiliar. Most REST APIs have separate endpoints for each resource — /courses, /users, /grades. Moodle does not. Every single API operation hits the same URL: your-moodle-site.com/webservice/rest/server.php. What changes is the wsfunction URL parameter — set it to 'core_course_get_courses' to get courses, 'core_user_get_users' to get users, 'gradereport_user_get_grade_items' to get grades, and so on. There are hundreds of possible wsfunctions.

This maps onto Bubble's API Connector in a specific way. You create one API Group with the full endpoint URL as the base URL. You add the wstoken (your authentication token) as a shared URL parameter marked Private — so it never reaches the browser. You add moodlewsrestformat=json as a shared parameter to force JSON responses (Moodle defaults to XML, which Bubble cannot parse). Then you create separate individual API Calls within the same group — one per wsfunction you need.

Moodle is an ideal candidate for a Bubble admin panel: IT departments can build custom dashboards showing course enrollment numbers, student grade summaries, and teacher workload data — all sourced live from the Moodle installation — without giving non-technical staff direct Moodle admin access. Bubble's Repeating Groups map naturally to Moodle's list responses, and Bubble's role-based conditions can hide sensitive grade data from lower-privilege users.

## Before you start

- A self-hosted Moodle installation where you have administrator access (Moodle Cloud Free tier also supported; requires admin credentials)
- Web Services and the REST protocol enabled in your Moodle admin panel (Site administration → Plugins → Web services)
- A Moodle External service created with the specific wsfunctions you need (at minimum: core_webservice_get_site_info for validation)
- A Moodle Web Services token (wstoken) generated for a user with appropriate Moodle roles
- The API Connector plugin installed in your Bubble app (Plugins tab → Add plugins → search 'API Connector' by Bubble)
- Bubble Starter plan or higher if you plan to use Backend Workflows for server-side token handling

## Step-by-step guide

### 1. Enable Web Services and generate a wstoken in Moodle

Log into your Moodle site as an administrator. Navigate to **Site administration → Plugins → Web services → Overview** — this page walks you through enabling web services step by step.

**Step 1 of the Moodle wizard — Enable Web Services:**
Go to Site administration → Advanced features → check 'Enable web services' → Save changes.

**Step 2 — Enable REST protocol:**
Go to Site administration → Plugins → Web services → Manage protocols → Enable the REST protocol (click the eye icon to make it visible).

**Step 3 — Create an External service:**
Go to Site administration → Plugins → Web services → External services → Add. Name it something like 'Bubble Integration'. Under this service, add the specific wsfunctions your Bubble app will use. At minimum add: `core_webservice_get_site_info` (for testing), `core_course_get_courses` (to list courses), `core_user_get_users` (to list users), `gradereport_user_get_grade_items` (for grades). Each function must be explicitly added — any wsfunction call NOT in the service returns an 'accessexception' error.

**Step 4 — Create a token:**
Go to Site administration → Plugins → Web services → Manage tokens → Add. Select the Moodle user the token will act as (typically a dedicated service account user with appropriate roles), select the External service you just created, optionally set an expiry date, then click Save. Copy the generated token string — you will paste it into Bubble's API Connector.

Store the token securely — it grants the same API access as the user it belongs to.

**Expected result:** Web Services are enabled, the REST protocol is active, an External service exists with your required wsfunctions listed, and you have a wstoken string ready to paste into Bubble.

### 2. Configure the API Connector with shared parameters

In your Bubble app, go to **Plugins tab → Add plugins**, search for 'API Connector' (by Bubble), and install it. Open the API Connector from the Plugins tab.

Click **Add another API** and name it **Moodle**.

Set the **Base URL** to: `https://your-moodle-site.com/webservice/rest/server.php` — replace 'your-moodle-site.com' with your actual Moodle installation domain.

Under **Shared URL Parameters** (at the API Group level, not the individual call level), add these two parameters:

**Parameter 1 — wstoken:**
- Key: `wstoken`
- Value: paste your wstoken string
- Check **Private**: YES — this marks the token as server-side only; it will never appear in browser network requests
- Check **Allow blank**: NO

**Parameter 2 — moodlewsrestformat:**
- Key: `moodlewsrestformat`
- Value: `json`
- Check **Private**: NO (not sensitive)
- Check **Allow blank**: NO

Do NOT add the wstoken as a header — Moodle's REST endpoint reads it as a URL parameter, not an Authorization header.

The resulting call structure for every Moodle API call will be:

```json
{
  "method": "GET",
  "url": "https://your-moodle-site.com/webservice/rest/server.php",
  "params": {
    "wstoken": "<private>",
    "moodlewsrestformat": "json",
    "wsfunction": "<function_name>"
  }
}
```

```
{
  "method": "GET",
  "url": "https://your-moodle-site.com/webservice/rest/server.php",
  "params": {
    "wstoken": "<private>",
    "moodlewsrestformat": "json",
    "wsfunction": "core_course_get_courses"
  }
}
```

**Expected result:** The Moodle API Group is created in Bubble's API Connector with the base URL pointing to /webservice/rest/server.php, wstoken is set as a shared Private parameter, and moodlewsrestformat=json is set as a shared non-private parameter.

### 3. Add API calls per wsfunction and run the Initialize call

Within the Moodle API Group, you'll create individual API Calls — one per Moodle wsfunction you need. Each call inherits the shared wstoken and moodlewsrestformat parameters automatically.

**First call — Site Info (for validation):**
Click **Add another call** inside the Moodle group. Configure it:
- Name: Get Site Info
- Method: GET
- Use as: Data
- Additional URL parameter: `wsfunction` = `core_webservice_get_site_info` (not Private)

Click **Initialize call**. Moodle will return a JSON object with your site name, version, user info, and supported wsfunctions. This is a safe, read-only call that validates your token. If you see `{"exception":"accessexception","errorcode":"accessexception","message":"..."}`  instead, the wsfunction was not added to your External service in Moodle admin.

**Second call — Get Courses:**
Add another call:
- Name: Get Courses
- Method: GET
- Use as: Data
- Additional parameter: `wsfunction` = `core_course_get_courses`

Run Initialize call. Bubble auto-detects the course array fields: id, fullname, shortname, categoryid, visible, etc.

**Third call — Get Users (optional, for grade views):**
Add another call:
- Name: Get Users
- Method: GET
- Use as: Data
- Additional parameters: `wsfunction` = `core_user_get_users`, plus a criteria parameter — Moodle's user search requires array-style params:
  - `criteria[0][key]` = `email`
  - `criteria[0][value]` = `<email>` (this will be dynamic when wired in workflows)

Note on Moodle's array parameters: Bubble's API Connector supports bracket notation in parameter keys — enter the key literally as `criteria[0][key]` and it passes correctly. Initialize this call with a real email address from your Moodle to get the response schema.

**Fourth call — Get Grades:**
Add another call:
- Name: Get Grade Items
- Method: GET
- Use as: Data
- Additional parameters: `wsfunction` = `gradereport_user_get_grade_items`, `courseid` = `<course_id>` (dynamic), `userid` = `<user_id>` (dynamic)

Initialize with real course and user IDs from your Moodle.

```
{
  "method": "GET",
  "url": "https://your-moodle-site.com/webservice/rest/server.php",
  "params": {
    "wstoken": "<private>",
    "moodlewsrestformat": "json",
    "wsfunction": "gradereport_user_get_grade_items",
    "courseid": "<course_id>",
    "userid": "<user_id>"
  }
}
```

**Expected result:** Each API call is initialized successfully. The 'Get Site Info' call returns your Moodle site name and version. The 'Get Courses' call returns an array of course objects with detected fields. The Initialize calls for all functions succeed with real response data.

### 4. Handle Moodle's HTTP-200 error responses in workflows

This step is critical and unique to Moodle. Unlike standard REST APIs that return 401 or 403 HTTP status codes on errors, Moodle always returns HTTP 200 — even when something went wrong. The error information is in the JSON body, in fields named 'exception' and 'errorcode'.

In Bubble, when an API call returns HTTP 200, Bubble treats it as success and tries to bind the response to your UI. If the actual response was an error JSON, Bubble will try to map the 'exception' and 'message' fields to your data types — causing blank pages, empty Repeating Groups, or silent failures.

**How to handle this in every Moodle workflow:**

After any Moodle API call action in a workflow, add a conditional branch:

- **Step 1:** Call API (e.g., Get Courses)
- **Step 2:** Add an 'Only when' condition to any subsequent actions: `Result of Step 1's exception is empty`
- **Alternative Step 2:** Add a separate workflow branch triggered when `Result of Step 1's exception is not empty` → show an Alert element with `Result of Step 1's message`

For Repeating Groups bound to Moodle API data: add a Text element below the Repeating Group with a conditional: 'This element is visible when API data's exception is not empty', displaying the error message. This gives users a visible indication when something went wrong.

**Common error codes to document for your team:**
- `accessexception`: the wsfunction is not in the External service — add it in Moodle admin
- `invalidtoken`: the wstoken is wrong or expired — regenerate in Moodle admin
- `invalidparameter`: a required parameter (like courseid or userid) was not provided or is wrong format
- `notloggedin`: the service account used for token generation doesn't have access to this function

**Expected result:** All Moodle-triggered workflows include error-checking conditionals that inspect the 'exception' field. Users see clear error messages when authentication or authorization fails, rather than blank pages with no indication of the problem.

### 5. Build the course enrollment dashboard

Create a page called `moodle-dashboard` in your Bubble app. This page will show a list of Moodle courses with enrollment information.

**Course list Repeating Group:**
Add a Repeating Group with Type set to 'Moodle - Get Courses' (the auto-detected data type from your API Connector initialization). Set Data Source to 'Get data from an external API → Moodle - Get Courses'.

In each cell, display:
- Course full name: `Current cell's fullname`
- Course short name: `Current cell's shortname`
- Visible status: conditional text showing 'Active' if `Current cell's visible = 1`, 'Hidden' otherwise
- A 'View Enrollments' button that navigates to an enrollment detail page passing the `Current cell's id` as a URL parameter

**Caching for WU efficiency:**
Rather than fetching the course list from Moodle's API on every page load (which costs WU each time), build a 'Sync Courses' Backend Workflow:
- Run Get Courses API call
- For each course in the response, create or update a Bubble 'MoodleCourse' Data Type record with id, fullname, shortname, categoryid, visible fields
- Bind the Repeating Group to the locally stored MoodleCourse Data Type instead of the live API

Add a 'Last synced' timestamp field and display it on the dashboard. A 'Sync Now' button triggers the Backend Workflow on demand. For real-time accuracy on critical data, trigger auto-sync on page load; for large Moodle installations, sync on a schedule.

**Date handling:**
Moodle timestamps are Unix epoch integers in seconds. Bubble expects milliseconds. In any workflow where you store a Moodle timestamp, multiply the value by 1000 using a Bubble Calculate expression before saving it as a date field.

**Expected result:** The Moodle dashboard displays a live list of courses from your Moodle installation. The course name, visibility status, and enrollment link are shown for each course. Error states are handled so that if the API returns an exception, a helpful message is shown rather than a blank page.

### 6. Set Bubble privacy rules and test the full integration

Before launching your Moodle-connected Bubble app, set privacy rules to protect any Moodle data you store locally in Bubble's database.

**Privacy rules for locally cached Moodle data:**
Go to Data tab → select any Data Type you created to cache Moodle data (e.g., MoodleCourse, MoodleEnrollment, MoodleGrade). Under the Privacy tab:
- For student-specific data (grades, enrollments): add a rule restricting visibility to the user whose Moodle user ID matches the record — `This Enrollment is visible when: Current User's moodle_user_id = This Enrollment's user_id`
- For institutional data (course lists, category structures): allow visibility to logged-in users but restrict the ability to modify records to admin users only
- For grade data: restrict to the student themselves or users with an 'Advisor' role — never expose all students' grades to all logged-in users

**Full integration test checklist:**
1. Log into your Bubble app and open the Moodle dashboard
2. Verify the course list loads — check that course names and IDs match your Moodle admin
3. Click a course and verify enrollment data loads for that course ID
4. Deliberately call a wsfunction not in your External service (temporarily remove one from Moodle admin) — verify your Bubble app shows an error message rather than a blank page
5. Verify the wstoken never appears in browser DevTools → Network tab (it should be absent since it's marked Private)
6. Check Bubble Logs tab → Workflow logs to confirm API calls are registering and WU usage is within expected range

**Expected result:** Privacy rules are active on all locally cached Moodle Data Types. The wstoken is confirmed absent from browser network requests. The full dashboard workflow — from login to course list to enrollment view — works end-to-end. Error states display helpful messages.

## Best practices

- Always add moodlewsrestformat=json as a shared parameter at the API Group level — never forget this. Without it, Moodle returns XML, which Bubble cannot parse, and the Initialize call will fail with a confusing error.
- Mark the wstoken as Private in the API Connector. This is not optional for production apps — the token has the same access level as the Moodle user it belongs to. A leaked wstoken gives anyone who finds it full API access to your Moodle data.
- Add an 'exception' field check to every workflow that calls Moodle API. Since Moodle returns HTTP 200 on errors, Bubble will not automatically handle error responses — you must build explicit error checking with 'Only when result's exception is empty' conditions.
- Cache frequently accessed Moodle data (course lists, category structures) in Bubble's built-in database using a sync workflow. Calling the Moodle API on every page load consumes Workload Units and adds latency — a local cache with a periodic sync is far more efficient.
- Create a dedicated Moodle service account user rather than using a personal admin account for token generation. Assign only the roles needed to call your required wsfunctions. This limits the impact if the token is ever compromised.
- For any wsfunction that requires array-style parameters (e.g., criteria[0][key] for user search), test each parameter combination in the API Connector Initialize tab before wiring to a workflow. Moodle's array parameter syntax must match exactly, and Bubble supports this notation correctly when entered literally.
- Set privacy rules on any Moodle data you store locally in Bubble's database — especially grade and enrollment data. Student academic records are personally identifiable information; ensure each user can only access their own records, not all students' data.
- Add a 'token last validated' timestamp to your Bubble app settings and display a warning if it hasn't been checked in 30+ days. Moodle tokens can be invalidated by admin actions (password changes, account disabling) without warning — periodic validation against core_webservice_get_site_info keeps your integration reliable.

## Use cases

### Custom LMS dashboard for department heads

A university IT team builds a Bubble dashboard for department heads who need to monitor course enrollment and student progress without accessing the Moodle admin interface. The Bubble app fetches course lists and enrollment counts via the API, displaying them in sortable tables. Department heads can filter by term, course category, and teacher — none of which requires a Moodle admin account.

Prompt example:

```
Build a Bubble dashboard that connects to a Moodle installation and shows a filterable list of courses with enrollment counts and teacher names. Department heads should be able to click a course to see a list of enrolled students and their completion percentages.
```

### Grade report portal for student advisors

A community college builds a Bubble portal where academic advisors can look up student grade summaries across multiple courses in one view — something the standard Moodle interface doesn't support efficiently. The Bubble app calls the gradereport_user_get_grade_items function for each student ID and displays a consolidated academic progress view with pass/fail flags.

Prompt example:

```
Create a Bubble app for academic advisors that accepts a student ID input, calls the Moodle API to fetch that student's grades across all enrolled courses, and displays a summary table with course names, grades, and pass/fail status.
```

### Automated enrollment notification system

An online training company uses Moodle for course delivery. They build a Bubble app that checks new enrollments via the API on a schedule and automatically sends welcome emails via SendGrid to newly enrolled users. This replaces a manual daily export-and-email process that their team was doing in spreadsheets.

Prompt example:

```
Build a Bubble automation that periodically calls the Moodle API to fetch recent enrollments, compares them against a locally stored list, identifies new enrollments, and triggers a SendGrid email to each newly enrolled student.
```

## Troubleshooting

### API call returns {"exception":"accessexception","errorcode":"accessexception"} despite the token working for other functions

Cause: The wsfunction is not added to the Moodle External service. Moodle's External service acts as a whitelist — only functions explicitly listed are callable with that token. This is not a token problem; the token is valid, but the specific function is blocked.

Solution: Log into Moodle admin → Site administration → Plugins → Web services → External services. Click 'Functions' next to your service. Search for the wsfunction you're trying to call and add it. Save. Re-test from Bubble — the error should resolve immediately.

### The Initialize call returns 'There was an issue setting up your call' or Bubble shows no response fields after initialization

Cause: Either the wstoken is wrong, the moodlewsrestformat=json parameter is missing (causing Moodle to return XML which Bubble cannot parse), or the Moodle site URL is incorrect in the Base URL.

Solution: First, confirm the moodlewsrestformat=json shared parameter is set at the API Group level. Then test the raw URL manually in a browser: visit https://your-moodle-site.com/webservice/rest/server.php?wstoken=YOUR_TOKEN&moodlewsrestformat=json&wsfunction=core_webservice_get_site_info — if this shows a valid JSON response, your token and URL are correct. If the browser shows XML, the json format parameter is missing. Re-initialize the Bubble API call after confirming the URL works in the browser.

### Repeating Group shows blank data even though the API call appears to succeed in the Initialize tab

Cause: The API call returned an HTTP 200 error response (e.g., {"exception":"..."}), which Bubble treats as a successful call. The response's 'exception' field doesn't match the expected data type fields, so the Repeating Group renders empty.

Solution: Add a Text element below the Repeating Group with the condition 'visible when API data's exception is not empty' and content set to 'API data's message'. This surfaces the error to you during development. The root cause is usually an accessexception (wsfunction not in service) or invalidtoken error — resolve those per the steps above.

### Backend Workflows option is missing from workflow actions and the 'Schedule API Workflow' action is not available

Cause: Backend Workflows require a paid Bubble plan. The Free plan does not include this feature.

Solution: Upgrade to Bubble's Starter plan ($32/month) or higher. After upgrading, go to Settings → API and enable 'This app exposes a Workflow API'. The Backend Workflows section will appear in the left sidebar. Note that on the Free plan, the wstoken Private parameter still protects the token from browser exposure for client-side API Connector calls, but server-side scheduling capabilities are unavailable.

### Moodle dates display as large numbers (e.g., 1717200000) instead of readable dates

Cause: Moodle returns timestamps as Unix epoch seconds (integer), but Bubble's date formatting expects milliseconds. A timestamp of 1717200000 seconds is being displayed as a raw number.

Solution: In the Bubble workflow where you store Moodle timestamps, add a Calculate step: multiply the Moodle timestamp value by 1000 before saving it to a Bubble date field. For display-only contexts, use a Bubble expression: [moodle_timestamp_field * 1000]:formatted as date. If you're using JavaScript (Toolbox plugin), use new Date(moodleTimestamp * 1000).toLocaleDateString().

## Frequently asked questions

### Do I need to mark the wstoken as Private in Bubble's API Connector?

Yes, and this is important. The wstoken grants API access equivalent to the Moodle user it belongs to. If you leave it unmarked (not Private), the token will appear in browser network requests and can be extracted by anyone viewing the page's network traffic. Mark it Private in the shared URL parameters at the API Group level — Bubble then only sends this parameter from its servers, never to the browser.

### Why does Moodle return HTTP 200 when my API call fails?

This is a design characteristic of Moodle's Web Services API. The HTTP status code 200 means 'the HTTP request was processed successfully' — it does not mean the Moodle function executed without errors. Moodle communicates function errors (wrong wsfunction, insufficient permissions, invalid parameters) through the JSON response body using 'exception' and 'errorcode' fields. In every Bubble workflow, you must explicitly check whether the response body contains an 'exception' field and handle the error case accordingly.

### How do I add more wsfunctions later without breaking my existing integration?

In Moodle admin, go to Site administration → Plugins → Web services → External services → click Functions next to your service → add the new wsfunction. In Bubble, add a new API Call within the existing Moodle API Group — it will automatically inherit the shared wstoken and moodlewsrestformat parameters. Run Initialize call for the new API Call to detect its response schema. You don't need to modify any existing calls.

### Can I use this Bubble integration with Moodle Cloud (not self-hosted)?

Yes. Moodle Cloud (moodle.com) supports Web Services on paid tiers. The integration steps are identical — enable Web Services, create an External service, generate a wstoken, and use your Moodle Cloud URL as the base URL in Bubble's API Connector. The Free Moodle Cloud tier may have limited Web Services functionality; check your plan's features.

### My Moodle installation has thousands of courses — will fetching all of them in a Repeating Group work?

The core_course_get_courses function returns all visible courses without pagination by default — for large installations this can be a very large response. Build a sync workflow instead: run the API call once, store results in a Bubble MoodleCourse Data Type, and bind your Repeating Group to the local data (with Bubble's built-in search and filtering). This is faster for users and avoids repeatedly calling the Moodle API. Use Bubble's 'Search for' with constraints to filter the local cache efficiently.

### Do I need Backend Workflows to use the Moodle API in Bubble?

Not strictly — the wstoken Private parameter works for client-side API Connector calls on all Bubble plans, keeping the token server-side. However, Backend Workflows are strongly recommended for production integrations: they enable scheduled syncs, server-side data processing, and more reliable error handling. Backend Workflows require Bubble's Starter plan ($32/month) or higher. On the Free plan, the API Connector still works for reading Moodle data, but you lose scheduling and advanced workflow capabilities.

---

Source: https://www.rapidevelopers.com/bubble-integrations/moodle
© RapidDev — https://www.rapidevelopers.com/bubble-integrations/moodle
