# LearnWorlds

- Tool: FlutterFlow
- Difficulty: Intermediate
- Time required: 45 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to LearnWorlds using an API Group with two required headers on every request: `Lw-Client` (your client ID) and `Authorization: Bearer {token}` from an OAuth2 client-credentials exchange. Run the token exchange in a Firebase Cloud Function — both credentials are secrets that must never live in Dart. Build a branded mobile academy that surfaces courses, enrollments, and certificates.

## Build a Branded Mobile Academy on Top of LearnWorlds

LearnWorlds is built for white-label education businesses — think academies, coaching programs, and professional training platforms that want full brand control. Its API v2 exposes courses, user enrollments, progress data, and its standout feature: built-in certificate generation. Surfacing these inside a native FlutterFlow app gives your learners a polished mobile experience that matches your brand identity rather than a generic web player.

The integration has one distinctive gotcha: every single request to the LearnWorlds API requires TWO headers simultaneously. The `Lw-Client` header carries your static client ID and must be present even with a perfectly valid bearer token — skip it and you get a 401. The `Authorization: Bearer {token}` header carries a short-lived OAuth2 access token obtained via a client-credentials flow. Forgetting either header is the number-one reason developers get stuck on LearnWorlds integrations, so this guide walks through setting both up as shared headers in an API Group before you add any endpoints.

API access is available on LearnWorlds' paid plans — check your plan tier against LearnWorlds' current pricing to confirm API access is included. Because both the client credentials and the bearer token are sensitive, they must live in a Firebase Cloud Function (or Supabase Edge Function), never in your FlutterFlow app's compiled code. FlutterFlow receives only a fresh token variable from the Cloud Function and passes it as the Authorization header value — keeping secrets off every learner's device.

## Before you start

- An active LearnWorlds paid plan with API access enabled (check your plan tier in LearnWorlds Settings → Developers)
- A FlutterFlow project with Firebase enabled (for the Cloud Function proxy) or a Supabase project if you prefer Edge Functions
- A Firebase project set up in FlutterFlow via Settings & Integrations → Firebase (if using the Firebase proxy path)
- Basic familiarity with FlutterFlow's API Calls panel and widget binding
- LearnWorlds API client credentials: client ID and client secret from Settings → Developers

## Step-by-step guide

### 1. Create a LearnWorlds API client and note your credentials

Log in to your LearnWorlds school admin panel and navigate to Settings → Developers (the exact path may be labeled API or Integrations depending on your plan). Create a new API client application — LearnWorlds will generate a client ID and a client secret. Copy both values and store them temporarily in a secure note; you will paste them into your Firebase Cloud Function environment variables in the next step, and they must never be placed anywhere else.

While you are in the Settings → Developers section, note the base URL for your account. The generic LearnWorlds API host is `https://api.learnworlds.com`, but some accounts use a per-school variant. Verify your actual host in the developer documentation or the API client setup screen before you build the API Group. Using the wrong base URL will result in 404 errors that look like auth failures, so confirm it now.

Also verify which API tier your LearnWorlds plan includes. API access is available on higher tiers; if your API client creation screen shows an upgrade prompt, you will need to upgrade before proceeding. Once you have your client ID, client secret, and confirmed base URL, you have everything needed to configure the Cloud Function proxy.

**Expected result:** You have a client ID and client secret saved securely, and you know the confirmed API base URL for your LearnWorlds account.

### 2. Deploy a Firebase Cloud Function to handle OAuth2 token exchange

LearnWorlds uses OAuth2 client-credentials flow: your server sends the client ID and secret to LearnWorlds' token endpoint, and gets back a short-lived bearer token. Because the client secret is sensitive, this exchange must happen on a server, not inside your Flutter app. A Firebase Cloud Function is the natural choice when your FlutterFlow project already uses Firebase.

In the Firebase Console for your project, navigate to Functions and create a new function (or use the Firebase CLI locally if you are comfortable with it). Set two environment variables in the function's configuration: `LW_CLIENT_ID` and `LW_CLIENT_SECRET` with the values from Step 1. Your function will POST to the LearnWorlds token endpoint, cache the resulting bearer token, refresh it before it expires, and return it to FlutterFlow along with the client ID so FlutterFlow can set both headers.

The function also acts as a proxy for actual API calls if you want full security — but at minimum it must handle token issuance. The Cloud Function endpoint is what FlutterFlow will call at the start of a user session to obtain a fresh token, which then gets stored in FlutterFlow's App State and passed as a header variable on every subsequent API call.

```
// Firebase Cloud Function — LearnWorlds token proxy
// index.js (Node.js 18+)
const functions = require('firebase-functions');
const axios = require('axios');

const LW_CLIENT_ID = process.env.LW_CLIENT_ID;
const LW_CLIENT_SECRET = process.env.LW_CLIENT_SECRET;
const LW_TOKEN_URL = 'https://api.learnworlds.com/oauth2/access_token';

let cachedToken = null;
let tokenExpiry = 0;

async function fetchToken() {
  if (cachedToken && Date.now() < tokenExpiry - 60000) {
    return cachedToken;
  }
  const response = await axios.post(LW_TOKEN_URL, {
    client_id: LW_CLIENT_ID,
    client_secret: LW_CLIENT_SECRET,
    grant_type: 'client_credentials'
  }, {
    headers: { 'Content-Type': 'application/json' }
  });
  cachedToken = response.data.access_token;
  tokenExpiry = Date.now() + (response.data.expires_in * 1000);
  return cachedToken;
}

exports.getLearnWorldsToken = functions.https.onRequest(async (req, res) => {
  try {
    const token = await fetchToken();
    res.json({ token, clientId: LW_CLIENT_ID });
  } catch (err) {
    console.error('Token fetch failed:', err.message);
    res.status(500).json({ error: 'Token exchange failed' });
  }
});
```

**Expected result:** The Cloud Function is deployed and returns a JSON object containing `token` and `clientId` when called. Test it in the Firebase Console using the test tool before moving to FlutterFlow.

### 3. Create the LearnWorlds API Group with dual shared headers

In FlutterFlow, click API Calls in the left navigation panel. Click + Add and select Create API Group. Name it 'LearnWorlds' and enter your confirmed LearnWorlds base URL (e.g. `https://api.learnworlds.com`) in the Base URL field.

Now configure two shared headers that will automatically be sent with every API call in this group. Click the Headers tab within the API Group. Add the first header: Name = `Lw-Client`, Value = click the variable icon and create a group-level variable called `lwClient`. Add the second header: Name = `Authorization`, Value = type `Bearer ` (with a trailing space) then click the variable icon and create a variable called `bearerToken`. Your Authorization header value field should read `Bearer {{bearerToken}}`.

These two variables will be populated at runtime by calling your Cloud Function from Step 2 first, storing the returned `clientId` as `lwClient` and the returned `token` as `bearerToken` in FlutterFlow App State, then passing them into the API Group when you trigger API calls from the Action Flow Editor. This is the complete dual-header setup — both headers will now be present on every endpoint you add to this group, which is exactly what LearnWorlds requires.

Save the API Group. You have not added any endpoints yet, but the shared header configuration is the critical foundation. Getting this right now prevents 401 errors on every subsequent endpoint.

```
// API Group shared headers configuration (reference)
{
  "group_name": "LearnWorlds",
  "base_url": "https://api.learnworlds.com",
  "shared_headers": [
    {
      "name": "Lw-Client",
      "value": "{{lwClient}}"
    },
    {
      "name": "Authorization",
      "value": "Bearer {{bearerToken}}"
    }
  ]
}
```

**Expected result:** The LearnWorlds API Group exists in FlutterFlow with two shared headers: Lw-Client and Authorization. Each header shows a variable placeholder icon next to the value field.

### 4. Add course and enrollment endpoints, test responses, and generate JSON Paths

With the API Group configured, add individual API calls for the LearnWorlds endpoints you need. Click the LearnWorlds API Group and select + Add API Call.

For a course catalog, create a GET call named 'Get Courses' with endpoint path `/v2/courses`. Add any query parameter variables you need for pagination (e.g. `page`, `itemsPerPage`). Click the Response & Test tab, paste in a sample LearnWorlds courses response (copy one from the LearnWorlds API docs or from a test call in a tool like Postman/Insomnia using your credentials), then click Generate JSON Paths. FlutterFlow will automatically detect fields like `$.data[*].id`, `$.data[*].title`, `$.data[*].description`, `$.data[*].image`, and `$.data[*].enrolled_users`. Name and save these paths — you will bind them to List tiles and Image widgets.

For user-specific enrollments, add a second GET call named 'Get User Courses' with endpoint `/v2/users/{{userId}}/courses`. Add a path variable `userId`. This lets you pull a specific learner's enrolled courses once you know their LearnWorlds user ID.

For the certificates feature (LearnWorlds' differentiator), add a GET call named 'Get User Certificates' with endpoint `/v2/users/{{userId}}/certificates`. Generate JSON Paths for fields like `$.data[*].title`, `$.data[*].issued_at`, `$.data[*].certificate_url`. You can bind the `certificate_url` to a URL launcher button so learners can open or share their certificate PDF.

After adding all endpoints, go back to each one, hit the Test button, and confirm you see a real 200 response. If you get a 401 at this stage, revisit Step 3 and check that both header variable names match exactly what you will pass at runtime.

```
// Sample JSON Paths for LearnWorlds /v2/courses response
// Paste these in the JSON Path fields after generating:

// Course list items:
// $.data[*].id          → course ID
// $.data[*].title       → course title
// $.data[*].description → course description
// $.data[*].image       → course cover image URL
// $.data[*].price       → course price

// Pagination:
// $.meta.total          → total course count
// $.meta.page           → current page number

// Certificates:
// $.data[*].title         → certificate name
// $.data[*].issued_at     → issue date
// $.data[*].certificate_url → PDF download link
```

**Expected result:** You have at least three API Calls in the LearnWorlds group (Get Courses, Get User Courses, Get User Certificates), each with a successful 200 test response and named JSON Paths ready for widget binding.

### 5. Wire the token fetch, App State, and header passing in the Action Flow

Now connect everything so the FlutterFlow app fetches a fresh token at startup and passes it into every API call. In your app's initial page (or an authentication gate page), open the Action Flow Editor. Add an API Call action targeting your Cloud Function URL from Step 2 (you can add the Cloud Function as a separate API Group named 'LearnWorlds Auth Proxy' with no shared headers, since it is your own endpoint). When the call succeeds, extract the `token` and `clientId` fields from the response and save them to App State variables: create `lwBearerToken` (String) and `lwClientId` (String) in App State.

Create a reusable Action or Page Action that you call before any LearnWorlds endpoint. This action calls the Cloud Function if `lwBearerToken` is empty or near expiry, then triggers the LearnWorlds API Group call passing `lwClientId` as the `lwClient` variable and `lwBearerToken` as the `bearerToken` variable.

In any widget where you want to show course data, go to the widget's Backend Query or use the API Call action from the Action Flow Editor, select the LearnWorlds group, select the endpoint, and map the App State variables to the group-level variables. FlutterFlow will include both headers automatically because they are shared headers on the group.

For the certificate download button, add an Open URL or Launch URL action in the Action Flow and bind the URL to the `$.data[*].certificate_url` JSON Path value from the Get User Certificates response — learners can then open their certificate PDF in the system browser.

Once all action flows are configured, use FlutterFlow's Test Mode to verify data appears in your widgets. Note that if you are testing on web, you may encounter CORS errors depending on LearnWorlds' allowed origins — in that case, route all API calls through the Cloud Function proxy rather than directly to `api.learnworlds.com` from the browser. Mobile native builds (iOS/Android) do not have this CORS restriction.

**Expected result:** Your FlutterFlow app fetches a fresh LearnWorlds token on startup, stores it in App State, and successfully passes both the Lw-Client and Authorization headers when loading courses, enrollments, and certificates in the UI.

### 6. Build the branded academy UI and connect widgets to the API data

With the API Group wired and data flowing, build out your branded academy screens. A typical LearnWorlds mobile academy in FlutterFlow has three core screens:

**Course Catalog screen:** Add a GridView or ListView to the screen. Set its backend query to the 'Get Courses' API Call. Bind the tile's Image widget to the `$.data[*].image` JSON Path, the Text widget for the course title to `$.data[*].title`, and a subtitle to `$.data[*].description`. Add a pagination bar or infinite scroll using the `$.meta.total` and `$.meta.page` paths to control which page of courses to fetch.

**My Learning screen:** After the user signs in with Firebase Auth, map their Firebase UID or email to their LearnWorlds user ID (store this mapping in Firestore after your first successful /v2/users lookup). Use 'Get User Courses' to fetch only their enrolled courses. Display progress indicators if the LearnWorlds API returns a progress percentage field for enrolled courses.

**Certificates screen:** Use 'Get User Certificates' to populate a ListView of earned certificates. Each list item can show the certificate title, issue date, and a teal 'Download' button that opens the `certificate_url` with a Launch URL action. This is LearnWorlds' strongest differentiator — surfacing certificates natively in a mobile app is something most competitors cannot offer out of the box.

Apply your brand colors and typography via FlutterFlow's Theme settings so the app feels like an extension of your LearnWorlds school rather than a generic app. If you need help architecting the full integration or run into complexity with the dual-header setup, RapidDev builds FlutterFlow integrations like this every week — free scoping call at rapidevelopers.com/contact.

**Expected result:** Your FlutterFlow app has a fully branded course catalog, enrolled courses screen, and certificates screen, all pulling live data from LearnWorlds via the dual-header API Group.

## Best practices

- Never put the LearnWorlds client ID or client secret in FlutterFlow Dart code or App Values — both are secrets that ship inside the compiled app bundle and can be extracted by any determined user.
- Run the OAuth2 client-credentials token exchange exclusively in a Firebase Cloud Function or Supabase Edge Function; FlutterFlow only ever sees the resulting short-lived bearer token.
- Cache the bearer token in App State with an expiry timestamp and refresh it proactively 60 seconds before expiry to avoid mid-session 401 errors.
- Configure the `Lw-Client` header as a shared group-level header in your API Group so it cannot accidentally be omitted from individual endpoint calls — the dual-header requirement applies to every single request.
- Map Firebase Auth users to LearnWorlds user IDs in Firestore (via a Cloud Function lookup on first login) rather than storing the LearnWorlds user ID client-side, so enrollment and certificate data stays correctly scoped per authenticated learner.
- Use deep-links or 'Launch URL' actions to send learners to the LearnWorlds web player for actual lesson video playback rather than trying to embed or re-implement the player in Flutter.
- Test on a physical device or APK before publishing — FlutterFlow's web preview may show CORS errors for direct calls to the LearnWorlds API that do not occur on native mobile builds.
- Verify your LearnWorlds plan tier supports the specific API endpoints you intend to use before building your full UI — some endpoints (advanced analytics, certain certificate controls) are gated to higher plans.

## Use cases

### White-label mobile academy for a professional training company

A professional certification body wants a branded iOS and Android app where members can browse course catalogs, track their enrolled courses, and download their earned certificates — all matching the company's design identity. FlutterFlow fetches courses and certificate data from LearnWorlds via API and renders them in fully branded widgets, while learners authenticate with Firebase Auth.

Prompt example:

```
Build a home screen that shows a grid of enrolled courses pulled from LearnWorlds, a certificates tab that lists completed certificates with a download button, and a profile screen showing the user's overall progress.
```

### Coaching program with gated module access

An online coach sells structured programs on LearnWorlds and wants a companion app where only paying members see module content. FlutterFlow checks enrollment status via the LearnWorlds enrollments endpoint and shows or hides content sections accordingly, with a deep-link to the LearnWorlds video player for actual lesson playback.

Prompt example:

```
Create a module list screen that shows each module as locked or unlocked based on the user's LearnWorlds enrollment, with a 'Continue Learning' button that opens the lesson URL in the system browser.
```

### Corporate L&D dashboard showing team certificate compliance

An HR team needs a mobile app where managers can see which employees have completed mandatory training and earned the required certificates on their LearnWorlds school. FlutterFlow calls the users and certificates endpoints, aggregates completion status per team, and flags overdue learners with a color-coded indicator.

Prompt example:

```
Build a team dashboard screen with a list of employees, each showing their certificate completion percentage and a red/green indicator, pulling live data from the LearnWorlds API.
```

## Troubleshooting

### 401 Unauthorized on every API call, even though the bearer token looks correct

Cause: The `Lw-Client` header is missing or misspelled. LearnWorlds requires BOTH `Lw-Client` and `Authorization: Bearer {token}` on every request — a valid bearer token alone is not sufficient.

Solution: Open your LearnWorlds API Group in FlutterFlow's API Calls panel and verify the Headers tab shows exactly two headers: `Lw-Client` (with the `{{lwClient}}` variable) and `Authorization` (with the value `Bearer {{bearerToken}}`). Check that both variables are being passed when you trigger the API call from the Action Flow Editor. Even a single missing space between 'Bearer' and the token causes a 401.

### Bearer token works initially but API calls start failing with 401 after some time

Cause: LearnWorlds bearer tokens are short-lived. If your Cloud Function is caching the token and not refreshing it before expiry, FlutterFlow receives and stores a stale token in App State that eventually stops working.

Solution: Update your Cloud Function to track the `expires_in` value from the token response and subtract 60 seconds from the expiry window before serving a cached token. In FlutterFlow, also store the expiry timestamp in App State and add a conditional check before any API call: if the stored token is near expiry, re-call the Cloud Function to get a fresh one before proceeding.

```
// Add to Cloud Function token cache logic:
const isExpiringSoon = Date.now() > tokenExpiry - 60000;
if (!cachedToken || isExpiringSoon) {
  await fetchToken();
}
```

### XMLHttpRequest error on web preview but the same API call works on a device or APK

Cause: CORS (Cross-Origin Resource Sharing) blocks browser-based API calls from FlutterFlow's web preview (and from your deployed web app) to `api.learnworlds.com` if LearnWorlds does not include your origin in its CORS allow-list.

Solution: Route all LearnWorlds API calls through your Firebase Cloud Function proxy rather than calling `api.learnworlds.com` directly from FlutterFlow. The Cloud Function is a server-to-server call and is not subject to CORS. Update your API Group base URL to point to your Cloud Function endpoint and have the function forward requests to LearnWorlds. Mobile native builds (iOS/Android) are not affected by CORS and can call the API directly if you prefer.

### API returns 404 on endpoints that look correct according to the documentation

Cause: Your LearnWorlds account uses a per-school API base URL that differs from the generic `https://api.learnworlds.com`. Some LearnWorlds plans use a school-specific API host.

Solution: Log into your LearnWorlds admin panel, go to Settings → Developers (or API), and check the exact API host shown for your account. Update the Base URL in your FlutterFlow API Group to match. Also verify you are on a plan tier that includes API access — a 404 on every endpoint can also indicate your plan does not have API enabled.

## Frequently asked questions

### Do I need both the Lw-Client header and the Authorization header on every request?

Yes — LearnWorlds requires both headers simultaneously on every API v2 request, without exception. The `Lw-Client` header carries your static client ID and `Authorization: Bearer {token}` carries the short-lived OAuth2 token. Missing either one results in a 401 Unauthorized response even when the other header is correct. Configure both as shared headers in your FlutterFlow API Group so they are automatically included on every endpoint.

### Can I store the client ID or client secret in FlutterFlow App Values?

No. App Values in FlutterFlow are constants compiled into the app bundle, which means anyone who downloads your app can extract them. Both the client ID and client secret must live exclusively in a Firebase Cloud Function (or Supabase Edge Function) environment variable. FlutterFlow should only ever receive and store the short-lived bearer token that the Cloud Function returns — and even that should not be persisted across app restarts.

### What happens when the bearer token expires mid-session?

LearnWorlds bearer tokens are short-lived. When a token expires, all API calls return 401 until a fresh token is obtained. To handle this gracefully, store the token's expiry timestamp alongside the token in FlutterFlow App State, and add a check before each API call action: if the token is about to expire, call the Cloud Function to get a new one first. Your Cloud Function should also proactively refresh its cached token 60 seconds before expiry.

### Can FlutterFlow receive LearnWorlds webhooks directly?

No — FlutterFlow compiles to a client app running on user devices, and it cannot act as a webhook server. LearnWorlds webhooks (for events like enrollment created or course completed) must be received by a Firebase Cloud Function or Supabase Edge Function, which can then write the event data to Firestore or a Supabase table. Your FlutterFlow app reads from that database rather than receiving webhook events directly.

### Does this integration work in FlutterFlow's web preview?

The API calls may fail in web preview with CORS errors if LearnWorlds does not allow browser origins in its CORS headers. To avoid this, route all API calls through your Firebase Cloud Function, which makes server-to-server calls that are not subject to CORS. On native iOS and Android builds, CORS is not enforced by the OS and direct API calls work fine.

### Which LearnWorlds plan tier do I need for API access?

LearnWorlds offers API access on its higher paid plans — the specific tier may change, so verify in your LearnWorlds account under Settings → Developers or on their current pricing page. If the API client creation screen shows an upgrade prompt, you will need to upgrade before you can obtain credentials. There is no free-tier API access.

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/learnworlds
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/learnworlds
