Connect FlutterFlow to LinkedIn Learning (formerly Lynda.com) through a Firebase or Supabase proxy that handles OAuth 2.0 and caches API responses. This integration requires an enterprise LinkedIn Learning license and approved Learning API access — not available to individual or free accounts. Cache course catalog and learner reporting data hourly in Firestore or Supabase to avoid high-latency reporting endpoints hitting rate limits.
| Fact | Value |
|---|---|
| Tool | Lynda (LinkedIn Learning) |
| Category | Social |
| Method | FlutterFlow API Call |
| Difficulty | Advanced |
| Time required | 60 minutes |
| Last updated | July 2026 |
Build a Corporate Learning Mobile App with LinkedIn Learning (Lynda) and FlutterFlow
If you found this page searching for 'Lynda' — Lynda.com became LinkedIn Learning in 2017. The brand name persists in searches, but the platform and API live under LinkedIn today. This integration is against `api.linkedin.com` with LinkedIn OAuth credentials.
The gating reality is critical to state up front: LinkedIn Learning API access requires an enterprise LinkedIn Learning license plus a LinkedIn developer app with Learning API access explicitly approved by LinkedIn. This is not self-serve. You apply at developer.linkedin.com, LinkedIn reviews the request, and approval can take days to weeks. Individual subscriptions and free accounts have no API path. If you are building an internal corporate learning app for an organization that already has an enterprise license, this integration is the right fit.
Once approved, the integration uses OAuth 2.0 in 2-legged (client credentials) mode for server-to-server calls. Your proxy authenticates with LinkedIn using the client ID and secret and makes calls on behalf of the organization. LinkedIn Learning's learner-reporting endpoints are notably high-latency — the standard architecture caches catalog and reporting data hourly in Firestore or Supabase, with the FlutterFlow app reading from the cache rather than calling LinkedIn on every screen open.
Integration method
FlutterFlow sends API requests to a Firebase Cloud Function or Supabase Edge Function that handles OAuth 2.0 (2-legged client credentials flow) and holds the LinkedIn Learning client secret. The proxy calls api.linkedin.com learning-catalog and learner-reporting endpoints, caches the results hourly in Firestore or Supabase, and returns cached data to the app. This keeps authentication server-side and prevents the high-latency reporting API from being called on every screen.
Prerequisites
- An enterprise LinkedIn Learning license for the organization you are building for (individual or free subscriptions do not qualify)
- A LinkedIn developer app at developer.linkedin.com with Learning API access applied for and approved
- OAuth 2.0 client credentials (client ID and client secret) from the approved LinkedIn developer app
- A Firebase project with Cloud Functions enabled, or a Supabase project with Edge Functions enabled
- A FlutterFlow project on a paid plan (API Calls and Custom Code require paid tiers)
Step-by-step guide
Confirm enterprise access and request LinkedIn Learning API approval
Before setting up any proxy or FlutterFlow configuration, confirm that two things are in place: (1) the organization has an active enterprise LinkedIn Learning license, and (2) a LinkedIn developer app with Learning API access is approved. To check the enterprise license: contact the organization's LinkedIn account manager or look in the LinkedIn Learning admin panel under Admin → Account → Subscription. If the organization is on an individual LinkedIn Premium plan rather than an enterprise Learning license, the API path is not available — LinkedIn Learning API is enterprise-only. To apply for API access: go to developer.linkedin.com and create a new LinkedIn application if you don't have one. In the application's Products section, look for the LinkedIn Learning API product and click Request Access. Fill in the use case description (a mobile corporate-learning app for internal use), the expected call volume, and the specific endpoints you need. LinkedIn reviews these requests manually. The review process typically takes several business days to a couple of weeks. Track the status in the developer portal. Do not proceed with building the proxy or FlutterFlow screens until API access is confirmed. The client credentials only work after the Learning API product is approved for the application — a valid-looking client ID and secret will still return 403 errors if the Learning API product is not approved. While waiting for approval, use this time to design your FlutterFlow screens and decide which endpoints you need (catalog, learner activity, assigned learning paths).
Pro tip: In your API access application, be specific about which endpoints you need and the size of your learner population. Vague applications are more likely to be delayed or rejected. Mention the internal corporate use-case and that you are building a mobile companion for existing licensed users.
Expected result: The LinkedIn developer app has Learning API product access approved. The Products section in the developer portal shows the Learning API with an active/approved status. The client ID and client secret are available from the app's Auth settings.
Deploy a Firebase Cloud Function that handles OAuth and caches data hourly
The proxy function serves two purposes. First, it handles the OAuth 2.0 client credentials flow — exchanging the client ID and secret for an access token and making calls to api.linkedin.com with that token. The client secret must never appear in the FlutterFlow app bundle. Second, it caches the API responses in Firestore (for Firebase) or Supabase (for Supabase Edge Functions) on an hourly schedule, so the FlutterFlow app reads from the fast local cache rather than hitting LinkedIn's high-latency reporting endpoints on every screen. For the caching strategy: deploy two functions — an HTTP function that the FlutterFlow app calls to read cached data, and a scheduled function (Firebase Cloud Scheduler or a Supabase cron job) that refreshes the cache from LinkedIn Learning every hour. The scheduled function runs independently of the app, ensuring that when any user opens the app, the data is already fresh without waiting for a LinkedIn API call. The example below shows the scheduled cache-refresh function. The HTTP function (called by FlutterFlow) simply reads from Firestore and returns the cached data. Store the LinkedIn client ID and secret in Firebase Secret Manager or as Firebase environment variables — not in the function source code.
1// Firebase Cloud Function: LinkedIn Learning cache refresh (scheduled)2// Runs every hour. Reads from LinkedIn, writes to Firestore.3// index.js (Node.js 18+)4const { onSchedule } = require('firebase-functions/v2/scheduler');5const { https } = require('firebase-functions/v2');6const { initializeApp } = require('firebase-admin/app');7const { getFirestore } = require('firebase-admin/firestore');8const fetch = require('node-fetch');910initializeApp();11const db = getFirestore();1213const LI_CLIENT_ID = process.env.LINKEDIN_CLIENT_ID;14const LI_CLIENT_SECRET = process.env.LINKEDIN_CLIENT_SECRET;15const LI_API = 'https://api.linkedin.com';1617async function getLinkedInToken() {18 const params = new URLSearchParams({19 grant_type: 'client_credentials',20 client_id: LI_CLIENT_ID,21 client_secret: LI_CLIENT_SECRET22 });23 const r = await fetch(`${LI_API}/oauth/v2/accessToken`, {24 method: 'POST',25 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },26 body: params.toString()27 });28 const data = await r.json();29 return data.access_token;30}3132// Scheduled function: refresh cache hourly33exports.refreshLearningCache = onSchedule('every 60 minutes', async () => {34 const token = await getLinkedInToken();35 const headers = { 'Authorization': `Bearer ${token}` };3637 // Fetch course catalog (check current LinkedIn Learning API endpoint paths)38 const catalogRes = await fetch(39 `${LI_API}/v2/learningAssets?q=criteria&assetFilteringCriteria.assetTypes[0]=COURSE`,40 { headers }41 );42 const catalog = await catalogRes.json();4344 // Write to Firestore45 await db.collection('linkedinLearning').doc('catalog').set({46 courses: catalog.elements || [],47 updatedAt: new Date().toISOString()48 });4950 console.log('LinkedIn Learning cache refreshed at', new Date().toISOString());51});5253// HTTP function: serve cached data to FlutterFlow54exports.learningProxy = https.onRequest(async (req, res) => {55 res.set('Access-Control-Allow-Origin', '*');56 if (req.method === 'OPTIONS') {57 res.set('Access-Control-Allow-Methods', 'GET');58 res.set('Access-Control-Allow-Headers', 'Content-Type');59 res.status(204).send('');60 return;61 }6263 const resource = req.query.resource || 'catalog';64 const doc = await db.collection('linkedinLearning').doc(resource).get();6566 if (!doc.exists) {67 res.status(404).json({ error: 'Cache not populated yet. Run the scheduled refresh.' });68 return;69 }7071 res.status(200).json(doc.data());72});Pro tip: If RapidDev handles the proxy deployment and cache setup, you can focus entirely on the FlutterFlow screens — their team builds enterprise FlutterFlow integrations like this regularly. Free scoping call at rapidevelopers.com/contact.
Expected result: Two Firebase functions are deployed: a scheduled `refreshLearningCache` that runs every hour and writes to Firestore, and an HTTP `learningProxy` that reads from Firestore and returns cached data to FlutterFlow. Running the scheduled function manually once populates the cache for initial testing.
Create a FlutterFlow API Group that reads from the cache proxy
With the proxy and cache in place, configure FlutterFlow to call the HTTP `learningProxy` function to read cached LinkedIn Learning data. In FlutterFlow, click API Calls in the left navigation panel. Click + Add → Create API Group. Name it `LinkedInLearning`. Set the Base URL to your deployed HTTP proxy URL — for example `https://us-central1-your-project.cloudfunctions.net/learningProxy` (Firebase) or `https://your-project.supabase.co/functions/v1/learning-proxy` (Supabase). Do not add authentication headers at the API Group level. The proxy function reads from Firestore and returns cached data without needing the FlutterFlow app to present credentials. If you want to restrict which users can call the proxy, add Firebase App Check or a shared API key header at the group level — but for an internal corporate tool, network-level restrictions (VPN, IP allowlist) may be sufficient. Inside the group, add an API Call named `GetCatalog`. Set Method to GET. In the Variables tab, add a variable named `resource` with default value `catalog`. Set the URL query parameter to `?resource={{ resource }}`. Click the Response & Test tab and run a test (ensure the scheduled function has run at least once to populate the cache). Paste the response from the proxy — which returns `{ courses: [...], updatedAt: '...' }`. Click Generate JSON Paths. FlutterFlow will generate paths for `$.courses`, `$.courses[0].title`, `$.courses[0].description`, `$.courses[0].durationInSeconds`, and other fields from the LinkedIn Learning catalog response. Name the paths clearly for binding in the next step. Add a second API Call named `GetLearnerActivity` with `resource: learnerActivity` (once you add the corresponding cache document from the learner-reporting endpoint to the scheduled function).
1// Example API Call config (reference only)2{3 "api_group": "LinkedInLearning",4 "base_url": "https://us-central1-YOUR-PROJECT.cloudfunctions.net/learningProxy",5 "calls": [6 {7 "name": "GetCatalog",8 "method": "GET",9 "url": "?resource=catalog",10 "json_paths": {11 "coursesList": "$.courses",12 "firstTitle": "$.courses[0].title.value",13 "firstDuration": "$.courses[0].durationInSeconds",14 "updatedAt": "$.updatedAt"15 }16 }17 ]18}Pro tip: The `updatedAt` timestamp in the cache response lets you show users when the data was last refreshed — add a small 'Last updated: X minutes ago' label to the catalog screen so users know they are looking at cached data.
Expected result: The `GetCatalog` API Call returns course data from the Firestore cache. The Response & Test tab shows the cached course list and generated JSON paths for title, duration, and other fields.
Create Data Types and build the course catalog screen
Create a FlutterFlow Data Type for a LinkedIn Learning course. Go to the Data Types section in the left navigation, click + Add Data Type, and name it `LearningCourse`. Add fields: `assetUrn` (String — the course's unique LinkedIn identifier), `title` (String), `shortDescription` (String), `durationSeconds` (Integer), `primaryAuthor` (String), `level` (String — BEGINNER, INTERMEDIATE, ADVANCED), `thumbnailUrl` (String). In the `GetCatalog` API Call settings, map the JSON paths from the LinkedIn Learning catalog response to the `LearningCourse` Data Type fields. Note that LinkedIn Learning's API returns localized text inside nested objects (e.g. `title.value` for the course title, `shortDescription.value` for the description) — map these in the JSON paths or flatten them in the proxy before returning. On the catalog screen, add a ListView or GridView powered by the `GetCatalog` API Call. Inside each list item, show the course thumbnail (via a Network Image widget bound to `currentCourse.thumbnailUrl`), the `title`, the author, the duration formatted as minutes (divide `durationSeconds` by 60 using a FlutterFlow math expression), and the `level` displayed as a colored Badge widget. Add a search bar at the top of the screen using a FlutterFlow Search bar widget with a local filter on the `title` field of the loaded course list. Since the data comes from cache (not a live API), local filtering is fast and doesn't require additional API calls. Add a header row showing the `updatedAt` timestamp from the catalog response formatted as 'Catalog updated X minutes ago' to set expectations for the user that this is cached data.
Pro tip: LinkedIn Learning course titles and descriptions use a locale-aware nested object (`{ value: 'Course Title', locale: { language: 'en', country: 'US' } }`). Flatten these in the proxy to return plain strings — it avoids JSON path complexity in FlutterFlow.
Expected result: The course catalog screen shows a scrollable list of LinkedIn Learning courses with thumbnails, titles, author names, durations in minutes, and level badges. The search bar filters the list locally. A 'Last updated' timestamp is visible at the top.
Add learner reporting data and set the hourly refresh schedule
The course catalog covers the browsing use-case. For the L&D management dashboard — completion rates, hours watched, learner progress — you need LinkedIn Learning's learner reporting API endpoints. These are the high-latency endpoints noted in the integration overview. Calling them on every screen open in a FlutterFlow app is not viable; the response times can reach several seconds, making the app feel broken. The solution is to extend the scheduled Firebase function to also fetch and cache learner reporting data alongside the catalog. Add a second Firestore document (e.g. `linkedinLearning/learnerActivity`) populated by the scheduled function with organization-level learning metrics. The exact LinkedIn Learning API endpoints for learner reporting depend on the access level granted to your approved developer app — check the LinkedIn Learning API documentation for your approved scope: common endpoints include `/v2/learningClassifications` and `/v2/organizationalEntityReportingActivities`. In FlutterFlow, add a second API Call to the `LinkedInLearning` API Group named `GetLearnerActivity` (GET, `?resource=learnerActivity`). Create a Data Type `LearnerActivity` with fields for employee name, courses completed, hours watched, and completion dates. Build the dashboard screen reading from this call with summary metrics at the top (total completions, total hours) and a ranked list of top courses below. For the scheduling: the Firebase Cloud Scheduler trigger `every 60 minutes` in the code example runs automatically after deployment. If you want to give the L&D manager a manual refresh option in the app, add a Refresh button that triggers an API Call to a separate Cloud Function that immediately runs the cache refresh (rather than waiting for the hourly schedule). This optional HTTP-triggered refresh function calls the same LinkedIn Learning endpoints and updates Firestore, then the app re-fetches from the `GetLearnerActivity` API Call.
1// Add to refreshLearningCache function: learner activity caching2// (inside the scheduled function, after the catalog fetch)34async function cachelearnerActivity(token) {5 const headers = {6 'Authorization': `Bearer ${token}`,7 'X-RestLi-Protocol-Version': '2.0.0'8 };910 // Replace with your approved LinkedIn Learning reporting endpoint11 // Endpoint paths vary by approval scope — check LinkedIn Learning API docs12 const activityRes = await fetch(13 `${LI_API}/v2/organizationalEntityReportingActivities` +14 `?q=criteria&organizationalEntityReportingCriteria.organizationalEntityType=ACCOUNT`,15 { headers }16 );17 const activity = await activityRes.json();1819 await db.collection('linkedinLearning').doc('learnerActivity').set({20 activities: activity.elements || [],21 updatedAt: new Date().toISOString()22 });23}2425// Call this from the scheduled function:26// await cacheLearnerActivity(token);Pro tip: LinkedIn Learning's reporting API can take 5-10 seconds per call in some regions. The scheduled function runs in the background where latency doesn't affect the user experience — this is exactly why the cache architecture is mandatory, not optional, for this integration.
Expected result: The scheduled function refreshes both catalog and learner activity data in Firestore every hour. The L&D dashboard screen shows total completions and hours watched from cached data. A manual refresh button triggers an immediate cache update on demand.
Common use cases
Corporate learning catalog browser for employees
Employees at a company use the app to browse LinkedIn Learning courses relevant to their role, bookmark courses, and see which learning paths are assigned to them. The FlutterFlow app reads the course catalog from the Firestore or Supabase cache (populated hourly by the proxy) and shows courses grouped by skill category with video thumbnails and duration.
Build a course catalog screen that shows LinkedIn Learning courses organized by skill category, with a thumbnail, title, author name, and duration for each course, along with a search bar to filter by keyword.
Copy this prompt to try it in FlutterFlow
Learner activity and completion dashboard for L&D managers
A Learning and Development manager uses the app to see team-level learning statistics: courses completed this month, hours watched, and top courses by engagement. The FlutterFlow app reads hourly-cached reporting data from the backend database and displays it in a summary dashboard with counts and a ranked list of top courses.
Create an L&D manager dashboard showing total courses completed, total hours watched, and the top 10 most-completed courses in the organization this month, with a refresh button that triggers a manual cache update from LinkedIn Learning.
Copy this prompt to try it in FlutterFlow
Assigned learning path tracker for a cohort
Managers assign a LinkedIn Learning path to a new employee cohort and track completion progress from the app. The FlutterFlow screen shows each employee's name and a progress bar indicating how far through the assigned path they are, pulling from the cached learner-reporting data updated hourly.
Design a cohort tracking screen that lists all employees in a group, showing each person's name, profile photo from LinkedIn, and a progress bar for their assigned learning path completion percentage.
Copy this prompt to try it in FlutterFlow
Troubleshooting
OAuth token request returns 401 or 'invalid_client' even though the client ID and secret are correct
Cause: The LinkedIn developer app does not have the Learning API product approved yet, or the client credentials flow is not enabled for the app. LinkedIn requires specific product approvals for different API scopes.
Solution: Check the Products section in the LinkedIn developer portal for your app. Confirm the Learning API product shows an approved or active status. If it shows 'pending' or is absent, the API access application is still in review. A valid client ID and secret will still return 401 if the Learning API product is not approved. For the 2-legged client credentials flow, also confirm that the app's OAuth settings include the client credentials grant type.
Learner reporting API calls return 403 Forbidden
Cause: The authenticated account's organization does not have the specific learner reporting scope enabled, or the API endpoint path does not match the approved scope for the developer app.
Solution: Contact LinkedIn's API support to confirm which reporting endpoints are included in your approved access. Different enterprise tiers may have access to different reporting endpoints. Verify the exact endpoint paths against the LinkedIn Learning API documentation for your approved product version. Note that LinkedIn API endpoint paths can change — always use the current documentation rather than examples from older tutorials.
Course catalog data is stale and does not reflect recent LinkedIn Learning catalog updates
Cause: The scheduled cache refresh function is not running as expected, or the Firestore document is not being updated.
Solution: Check the Firebase Functions logs for the `refreshLearningCache` function to see if it is executing on schedule and whether it returned any errors. Common issues are expired OAuth tokens stored in wrong environment variables, or Firestore write permission errors. Trigger the function manually from the Firebase console to test it. Also verify that the Cloud Scheduler trigger is active in the Firebase console — scheduled functions can be paused.
XMLHttpRequest error in FlutterFlow web preview when calling the proxy
Cause: The Firebase Cloud Function is not returning CORS headers, so the browser blocks the response in FlutterFlow's web Run mode.
Solution: Add `res.set('Access-Control-Allow-Origin', '*')` to the `learningProxy` HTTP function before any response, and handle OPTIONS preflight requests by returning 204 with the Allow headers. The example code in Step 2 includes this CORS handling. Native Android and iOS builds are not affected by CORS.
Best practices
- Confirm enterprise license and API approval before writing a single line of proxy code. Building without confirmed access results in 401/403 errors that look like technical bugs but are actually access-control rejections.
- Never place LinkedIn OAuth client credentials in a FlutterFlow API Call header or in Dart code. The client secret grants access to organization-wide learning data and must stay in Firebase Secret Manager or Supabase secrets.
- Cache course catalog and learner reporting data hourly in Firestore or Supabase. LinkedIn Learning's reporting endpoints have latency that makes per-screen API calls unacceptable — the cache architecture is mandatory for a responsive app.
- Refresh the cache on a server-side schedule (Firebase Cloud Scheduler or Supabase cron), not by having the FlutterFlow app trigger a cache refresh on every open. Scheduled server-side refreshes ensure the data is ready before any user opens the app.
- Flatten LinkedIn Learning's nested localized text objects (`{ value: 'Course Title', locale: {...} }`) in the proxy before returning them to FlutterFlow. This simplifies JSON path binding and avoids locale-selection logic in the widget tree.
- Show users a 'Last updated' timestamp from the cache so they understand the data latency. An L&D dashboard showing data from the last hourly refresh is entirely appropriate — but surprises if not communicated.
- Add a manual cache refresh option (an HTTP-triggered Firebase function) for L&D managers who need fresher data than the hourly schedule provides, while keeping the FlutterFlow app itself reading only from the fast Firestore cache.
- Use the minimum LinkedIn Learning API scopes your use-case requires. Request only catalog and reporting access — not user management or connection data — to limit the blast radius if credentials are ever compromised.
Alternatives
Meetup's community-events GraphQL API is accessible without enterprise gating and suits learning communities and in-person workshop tracking — a practical alternative if the LinkedIn Learning enterprise license requirement is not met.
Agorapulse's social management API covers LinkedIn publishing and analytics at an account level without requiring an enterprise Learning license, making it the right choice if the goal is LinkedIn content management rather than learning platform integration.
Later's scheduling API is a simpler, self-serve integration that covers LinkedIn posts and analytics without any enterprise approval process, suitable if the app's LinkedIn use-case is content scheduling rather than learning data.
Frequently asked questions
Will this integration work if I have an individual LinkedIn Premium subscription instead of an enterprise LinkedIn Learning license?
No. LinkedIn Learning API access is enterprise-only. An individual LinkedIn Premium or Learning subscription does not grant API access. The API requires an organizational enterprise LinkedIn Learning license (typically purchased by a company for multiple seats) plus explicit approval of the Learning API product for a LinkedIn developer app. If you are an individual developer without an enterprise license, LinkedIn Learning API is not accessible.
Can I place the LinkedIn OAuth client secret directly in a FlutterFlow API Call header?
No. FlutterFlow compiles to a Flutter client app. Any value in an API Call header or Dart code is embedded in the compiled binary and can be extracted by anyone who downloads the app. The LinkedIn Learning client secret grants access to organization-wide learning data and learner activity for all employees — it must stay exclusively in a server-side environment variable (Firebase Secret Manager or Supabase secrets).
Why does the learner reporting API call take so long to respond?
LinkedIn Learning's learner reporting endpoints aggregate data across large organizational datasets and are not optimized for real-time request-response latency. Response times of 5-10 seconds are common. This is expected behavior, not a bug. The standard architecture — and the one described in this tutorial — is to call these endpoints on a scheduled hourly basis from a backend function and cache the results. The FlutterFlow app then reads from the fast local cache (Firestore or Supabase) rather than waiting for a slow reporting API response on every screen open.
How do I search the LinkedIn Learning course catalog within the app?
Because the catalog data is cached in Firestore or Supabase, you have two options. The first is client-side filtering: load the full cached course list into a FlutterFlow App State variable and use FlutterFlow's local search filter on the list. This is fast and requires no additional API calls. The second option is server-side search: add a search parameter to the scheduled function and cache multiple keyword-based result sets, or add a Firestore full-text search using an extension like Algolia or Typesense. For a typical corporate catalog of hundreds of courses, client-side filtering on the cached list is sufficient and simpler.
How often does the LinkedIn Learning course catalog change, and how often should I refresh the cache?
Course catalog data is relatively stable — new courses are added weekly, not hourly. Hourly cache refreshes are more frequent than necessary for catalog data alone; you could refresh the catalog daily and use hourly refreshes only for the learner activity and reporting data. Adjust the scheduled function to write catalog data less frequently to reduce LinkedIn API calls and Firebase function costs, while keeping reporting data fresh on the hourly schedule.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation