# Lynda (LinkedIn Learning)

- Tool: Bubble
- Difficulty: Advanced
- Time required: 3–5 hours (plus 1–4 weeks for LinkedIn API approval)
- Last updated: July 2026

## TL;DR

LinkedIn Learning (formerly Lynda.com) has a gated enterprise API that requires both an active organizational license and a separately approved LinkedIn developer application. Once approved, Bubble connects via the API Connector using OAuth 2.0 two-legged client credentials — no user login required. All token exchange and learner data fetching must run server-side in Backend Workflows, and course catalog data should be cached hourly in Bubble's database to avoid slow, high-latency reporting calls.

## Building a Corporate Learning Portal with LinkedIn Learning and Bubble

LinkedIn Learning — still commonly searched as 'Lynda' — is the enterprise e-learning platform that organizational L&D teams use to assign courses, track completion, and measure skill development across their workforce. The Bubble use-case is building a branded corporate learning portal: employees browse the course catalog, see their assigned learning paths, and managers view team completion dashboards — all within a Bubble app that reflects your company's branding rather than the default LinkedIn Learning interface.

The critical reality to understand before writing a single line of configuration: LinkedIn Learning has two separate access gates that must both be satisfied. First, your organization must hold an active enterprise LinkedIn Learning seat license — this is the organizational plan, not an individual LinkedIn Premium subscription. Second, you must apply for and receive approval for LinkedIn Learning API access through LinkedIn's developer portal at developer.linkedin.com. These are independent gates: having the license does not automatically grant API access. The developer app approval process can take anywhere from days to weeks depending on LinkedIn's review queue and your stated use case.

Once both gates are cleared, the integration uses OAuth 2.0 two-legged client credentials — the simplest OAuth flow for server-to-server calls. Your app exchanges a client_id and client_secret for a Bearer access token, which then authenticates all API calls. Tokens expire approximately every 60 minutes, so a scheduled Backend Workflow handles automatic refresh. Because Bubble's API Connector runs calls server-side, your client secrets remain protected in Private headers and never appear in the browser. Learner reporting data has significant processing lag on LinkedIn's side, so the architecture caches course and learner data in Bubble's database and refreshes it on a schedule rather than making live API calls that would block the UI and incur high latency.

## Before you start

- Active enterprise LinkedIn Learning organizational seat license (not individual LinkedIn Premium)
- Approved LinkedIn developer application with Learning API access scope from developer.linkedin.com (allow 1–4 weeks for review)
- LinkedIn developer app credentials: client_id and client_secret
- Bubble app on a paid plan (Starter or above) — Backend Workflows required for token exchange and scheduled refresh are not available on the free tier
- API Connector plugin installed in your Bubble app (Plugins → Add plugins → search 'API Connector' by Bubble)

## Step-by-step guide

### 1. Verify Both Access Gates and Apply for API Approval

Before configuring anything in Bubble, confirm that your organization has an active enterprise LinkedIn Learning seat license through your LinkedIn account executive or procurement team. This is separate from individual LinkedIn Premium subscriptions — it is the organizational plan that grants access to LinkedIn Learning's admin portal at linkedin.com/learning/admin.

Once you have confirmed the license, go to developer.linkedin.com and create a LinkedIn developer application. In the Products section of your app settings, request access to 'Learning API'. You will need to describe your integration use case — explain that you are building an internal learning portal on Bubble that will surface course catalog and learner progress data to authorized employees within your organization.

The review process is not instant. LinkedIn's team manually reviews Learning API access requests, and approval timelines range from a few business days to several weeks. Do not begin building the Bubble integration structure until you receive approval confirmation and can generate a valid access token — without it, all API calls will return 401 Unauthorized regardless of correct configuration.

While waiting for approval, sketch your Bubble data model (described in Step 4) and identify which LinkedIn Learning endpoint responses you will need to map to Bubble fields. This preparation time is productive and means you can move quickly once credentials arrive.

**Expected result:** You have a LinkedIn developer app in 'approved' status with Learning API access scope, and you have your client_id and client_secret ready to use.

### 2. Build the OAuth Token Exchange Backend Workflow

LinkedIn Learning uses OAuth 2.0 two-legged client credentials — your Bubble app exchanges a client_id and client_secret for a time-limited Bearer token without requiring any user login. This exchange must happen in a Bubble Backend Workflow because the client_secret cannot appear in a client-side action.

Go to your Bubble editor and navigate to the Backend Workflows section (Settings → API → enable 'This app exposes a Workflow API', then click 'Backend Workflows' in the left menu). Create a new API Workflow named 'Refresh LinkedIn Token'.

In this workflow, add a 'Make an API request' step targeting LinkedIn's token endpoint. Configure it as a POST to https://www.linkedin.com/oauth/v2/accessToken with body type 'Form parameters URL-encoded' and the following parameters:
- grant_type: client_credentials
- client_id: [your LinkedIn app client_id]
- client_secret: [your LinkedIn app client_secret]

Mark both client_id and client_secret as sensitive values. The response will contain an access_token string and an expires_in value in seconds (typically 3600).

Next, create a Bubble data type called LinkedInToken with fields: access_token (text), expires_at (date), is_active (yes/no). In the workflow, after the API call, use 'Create a new thing' or 'Make changes to a thing' to store the returned access_token and calculate expires_at as 'Current date/time + expires_in seconds'.

Run this Backend Workflow manually once to generate your first token. Verify the token was stored correctly by checking the Data tab → LinkedInToken.

```
POST https://www.linkedin.com/oauth/v2/accessToken
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=<your_client_id_private>
&client_secret=<your_client_secret_private>

// Response:
{
  "access_token": "AQX...",
  "expires_in": 3600
}
```

**Expected result:** A LinkedInToken record exists in your Bubble database with a valid access_token and an expires_at timestamp approximately 60 minutes in the future.

### 3. Configure the API Connector for LinkedIn Learning Calls

With a valid token stored in your Bubble database, configure the API Connector to authenticate against LinkedIn's API dynamically.

Go to Plugins → API Connector → Add another API. Name it 'LinkedIn Learning'. Set the shared header:
- Key: Authorization
- Value: Bearer [search for LinkedInToken's access_token where is_active = yes]
- Check the 'Private' checkbox

In Bubble's API Connector, the header value can reference a dynamic value using the 'Use as' field set to 'Data' for lookup calls or 'Action' for write calls. For the Authorization header specifically, you will set the default value to the stored token string and update it whenever the token refreshes.

Set the base URL to https://api.linkedin.com/v2. Add a second shared header:
- Key: X-Restli-Protocol-Version
- Value: 2.0.0

This version header is required by LinkedIn's REST API. Add a third shared header:
- Key: LinkedIn-Version
- Value: 202501

This pins the API version. Now add your first call: name it 'Get Learning Assets', set method to GET, and URL path to /learningAssets. Add query parameters: assetFilteringCriteria.assetTypes[0]=COURSE and count=25.

Click 'Initialize call'. For the initialize to succeed, you need a real, valid access token in the Authorization header. Use the token you stored in Step 2. If initialization succeeds, Bubble will detect the response fields automatically.

```
{
  "method": "GET",
  "url": "https://api.linkedin.com/v2/learningAssets",
  "headers": {
    "Authorization": "Bearer <dynamic_token_from_db_private>",
    "X-Restli-Protocol-Version": "2.0.0",
    "LinkedIn-Version": "202501"
  },
  "params": {
    "assetFilteringCriteria.assetTypes[0]": "COURSE",
    "count": "25",
    "start": "<dynamic_offset>"
  }
}
```

**Expected result:** The API Connector shows detected response fields for LinkedIn Learning assets — including fields like urn, title, details.descriptionIncludingHtml, details.timeToComplete, and asset type.

### 4. Build the Cached Course Catalog Backend Workflow

LinkedIn Learning's learner reporting endpoints are notably high-latency — calling them live on user-triggered page loads creates a poor experience and burns Bubble Workload Units unnecessarily. The correct architecture caches the course catalog in Bubble's database and refreshes it on a schedule.

Create a Bubble data type called CourseCatalog with fields: course_urn (text), title (text), description (text), thumbnail_url (text), duration_seconds (number), skill_tags (list of text), last_synced (date).

Create a Backend Workflow named 'Sync Course Catalog'. In this workflow:
1. Add a step to call the 'Get Learning Assets' API Connector call with start=0 and count=25.
2. For each result in the response array, use 'Make changes to a thing' to upsert into CourseCatalog (search for existing record by course_urn; create if not found, update if found).
3. Add a 'Schedule API Workflow' step that schedules the same workflow with start=25 to fetch the next page, conditional on the response containing a 'next' cursor.

Schedule this Backend Workflow to run on a recurring basis: Workflow editor → Schedule a recurring event → set to run every 60 minutes. This keeps the catalog reasonably fresh without hammering the API.

For learner progress data, create a separate Backend Workflow named 'Sync Learner Progress'. This calls the learner progress endpoint and stores completion percentages in a LearnerProgress data type with fields: learner_id, course_urn, completion_percentage, last_viewed_at, last_synced. Schedule this to refresh every 6 hours — progress data does not change minute-to-minute.

Configure Bubble privacy rules on both data types: Data tab → CourseCatalog → Privacy → add a rule that only logged-in users (or users with a specific role) can view records. This is critical if any CourseCatalog field contains pricing or licensing data from your enterprise agreement.

```
// CourseCatalog data type structure
{
  "course_urn": "urn:li:lyndaCourse:12345",
  "title": "Learning Python",
  "description": "Master Python programming fundamentals...",
  "thumbnail_url": "https://media.licdn.com/...",
  "duration_seconds": 7200,
  "skill_tags": ["Python", "Programming"],
  "last_synced": "2026-07-10T09:00:00Z"
}
```

**Expected result:** Your CourseCatalog data type in Bubble contains course records populated by the sync workflow, visible in the Data tab. The scheduled workflow runs every 60 minutes and updates existing records by URN.

### 5. Build the Learner Progress Dashboard

With course catalog and learner data cached in Bubble's database, build the UI components that read from Bubble's DB — never making live API calls that block page renders.

For the course catalog browser: add a Repeating Group to your page with data source 'Search for CourseCatalog'. Add filter inputs (a dropdown for skill_tag, a text search on title using 'contains') that drive Bubble-side search constraints. Each row shows the thumbnail_url as a dynamic image, title, duration_seconds formatted as hours and minutes, and a 'View Course' button that links to the LinkedIn Learning course URL (construct from the URN).

For the learner progress view: add a second Repeating Group with data source 'Search for LearnerProgress where learner_id = Current User's employee_id'. Display a progress bar element (use Bubble's built-in progress bar or a custom CSS element) driven by the completion_percentage field. Show last_viewed_at as a formatted date.

For the manager dashboard: add a data source filtered to 'Search for LearnerProgress where learner_id is in [list of direct report IDs]'. Display aggregate stats using Bubble's expression engine — average completion percentage, count of completions above 100%, count of users with 0% on any assigned course.

Add a 'Last updated' text element on every screen that shows the most recent last_synced value from the data type. This sets honest expectations: users understand they are seeing cached data, not a live feed. LinkedIn Learning's processing lag means even live API calls would not reflect activity from the past 24–72 hours.

If your organization needs to show learner progress to individual employees in near-real-time (within minutes of course completion), reconsider the architecture — LinkedIn's API is not designed for sub-minute polling and the latency makes live display inaccurate regardless of how frequently you poll.

**Expected result:** The course catalog Repeating Group displays courses from Bubble's CourseCatalog table, filterable by skill and searchable by title. The learner progress section shows each user's completion percentage with a visible 'last updated' timestamp.

### 6. Schedule Token Refresh and Monitor Integration Health

With the data model built, the final configuration step is ensuring the OAuth token refreshes automatically and the integration remains stable in production.

In the Bubble Backend Workflows editor, schedule the 'Refresh LinkedIn Token' workflow as a recurring event. Set it to run every 50 minutes — LinkedIn tokens typically expire after 60 minutes, so refreshing at 50 minutes provides a 10-minute buffer. In the workflow, after storing the new token, update the previous LinkedInToken record's is_active field to 'no' and mark the new record as is_active = 'yes'. This ensures the API Connector always picks up the most current token.

Note: scheduling recurring Backend Workflows requires a paid Bubble plan (Starter $32/month or above). If you are on Bubble's free tier, this integration cannot run in production — the token will expire and all API calls will return 401 after the first hour.

To monitor integration health: go to Logs tab → API Logs in Bubble. Any failed LinkedIn Learning calls appear here with HTTP status codes (401 = expired token, 403 = scope not approved or license issue, 429 = rate limit). Set up a Backend Workflow that emails your admin account if a LinkedIn Learning call returns a non-200 status — Bubble's conditional workflow steps support this pattern.

RapidDev's team has built learning portals on Bubble with complex API integrations including token lifecycle management — if you need help architecting the refresh logic or the data caching layer, book a free scoping call at rapidevelopers.com/contact.

```
// Recurring Backend Workflow schedule
// Name: Refresh LinkedIn Token
// Frequency: Every 50 minutes
// Time zone: UTC (recommended for consistency)

// Workflow steps:
// 1. POST https://www.linkedin.com/oauth/v2/accessToken (client_credentials)
// 2. Make changes to LinkedInToken (is_active = 'yes') for new token
// 3. Make changes to LinkedInToken (is_active = 'no') for all previous tokens
```

**Expected result:** The token refresh Backend Workflow runs every 50 minutes. Your Bubble Logs tab shows no 401 errors from LinkedIn Learning. The LinkedInToken data type always has exactly one is_active = yes record with a future expires_at timestamp.

## Best practices

- Satisfy both access gates before building: confirm the enterprise LinkedIn Learning license is active AND the LinkedIn developer app has Learning API approval. Attempting to build the integration without both will result in 401/403 errors with no path to resolution through configuration changes alone.
- Always mark your LinkedIn client_id and client_secret as Private in the API Connector — Bubble proxies all calls server-side, so these credentials are already protected, but the Private checkbox provides an additional safeguard against them appearing in Bubble's editor logs.
- Cache course catalog data in a Bubble data type refreshed hourly — LinkedIn Learning's catalog changes slowly, and live API calls on every page load create unnecessary latency and WU consumption. Never call the learner reporting endpoints synchronously in user-triggered workflows.
- Use LinkedIn Learning URNs (urn:li:lyndaCourse:12345) as the stable primary key in your CourseCatalog data type, not human-readable titles. URNs are stable across course updates; titles can change when LinkedIn refreshes course metadata.
- Configure Bubble privacy rules on all data types storing LinkedIn Learning data before going live. CourseCatalog and LearnerProgress records should only be readable by logged-in users — or by users with a specific 'employee' role if your Bubble app has role-based access.
- Monitor token health with a visual indicator in your admin panel showing the current token's expires_at value and is_active status. This makes it immediately obvious when the refresh workflow has stopped running, before it causes production failures.
- Schedule the token refresh at 50 minutes, not 60 — the 10-minute buffer accounts for Bubble's scheduler timing variance and prevents race conditions where a call fires microseconds after expiry.
- Display the last_synced timestamp prominently on every screen showing LinkedIn Learning data. Set honest expectations: users seeing completion percentages should understand the data may be hours old due to LinkedIn's processing lag.

## Use cases

### Branded Corporate Learning Portal

Build a custom-branded learning portal where employees browse the company's LinkedIn Learning course catalog, view their assigned learning paths, and track their own completion percentages — all within a Bubble app that matches your internal design system instead of the default LinkedIn Learning interface.

### L&D Manager Completion Dashboard

Create a manager-facing dashboard that shows team-level course completion rates, overdue learning paths, and individual learner progress pulled from the cached LinkedIn Learning data in Bubble's database, refreshed hourly by a scheduled Backend Workflow.

### Skills Gap Tracker

Combine LinkedIn Learning course data with internal role skill requirements stored in Bubble's database to surface a per-employee skills gap view: which courses from the catalog map to missing skills, what's assigned, and what's been completed.

## Troubleshooting

### All LinkedIn Learning API calls return 401 Unauthorized even with a stored token

Cause: The Bearer token has expired (60-minute lifespan) and the refresh Backend Workflow has not run, or the is_active lookup in the Authorization header is returning no value.

Solution: Go to Bubble Data tab → LinkedInToken. Check if any record has is_active = yes and expires_at in the future. If not, manually trigger the 'Refresh LinkedIn Token' Backend Workflow. If the workflow itself fails, verify your client_id and client_secret are still valid in the LinkedIn developer portal — credentials can be rotated by LinkedIn admin changes.

### LinkedIn developer portal shows 'Product not enabled' or API calls return 403 Forbidden

Cause: One or both access gates are not satisfied: either the LinkedIn Learning API product has not been approved in your developer app, or your organization's LinkedIn Learning enterprise license has lapsed or changed.

Solution: Go to developer.linkedin.com → your app → Products tab. Confirm 'Learning API' shows status 'Approved'. If it shows 'Pending' or 'Not requested', the approval process is incomplete. Contact your LinkedIn account executive to confirm the organizational seat license is active. Both conditions must be true simultaneously — an approved app with a lapsed license returns 403.

### 'There was an issue setting up your call' error when initializing the API Connector

Cause: The Initialize call requires a real, valid Bearer token in the Authorization header — it makes a live request to LinkedIn's API. If the token is expired or malformed at initialization time, Bubble cannot detect the response structure.

Solution: Before clicking Initialize, manually run the 'Refresh LinkedIn Token' Backend Workflow to generate a fresh token. Copy the raw access_token string from the LinkedInToken record in Bubble's Data tab. Paste it directly into the Authorization header default value in the API Connector (replacing any dynamic reference temporarily). Click Initialize. After successful initialization, restore the dynamic reference to the stored token.

### Learner progress data is always zero or stale, never reflecting recent course activity

Cause: LinkedIn Learning's learner reporting endpoints have a processing lag of 24–72 hours. Recent course completions are not immediately reflected in API responses.

Solution: This is expected LinkedIn API behavior, not a Bubble configuration issue. Update your UI to display a 'Data reflects activity through [date]' label next to progress percentages. Do not attempt to reduce the staleness by polling more frequently — LinkedIn's rate limits will trigger 429 errors, and the underlying data processing lag is server-side at LinkedIn and cannot be bypassed.

### Backend Workflow scheduling option is greyed out or unavailable

Cause: Recurring Backend Workflow scheduling requires a paid Bubble plan. Free plan users cannot set recurring schedules.

Solution: Upgrade to at least the Bubble Starter plan ($32/month) to unlock recurring Backend Workflow scheduling. Without this, the OAuth token will expire after 60 minutes and the integration will silently stop working. There is no workaround on the free tier for a production LinkedIn Learning integration.

## Frequently asked questions

### Does my organization need to pay for LinkedIn Learning before I can build this integration?

Yes — an active enterprise LinkedIn Learning seat license is the first of two mandatory requirements. Individual LinkedIn Premium subscriptions (even those that include LinkedIn Learning access) do not qualify. You need the organizational plan sold to companies, not individuals. Contact your LinkedIn account executive or check linkedin.com/learning/admin to confirm your organization's license status.

### How long does the LinkedIn Learning API approval process take?

The review process is manual and LinkedIn provides no guaranteed timeline. In practice, straightforward internal portal use cases are typically approved within a few business days to two weeks. Requests that are vague about data usage or suggest redistribution of LinkedIn content to unauthenticated users may take longer or be declined. Write a specific, honest use case description in your developer app application.

### Can I use this integration on Bubble's free plan?

No. The token refresh logic requires scheduling recurring Backend Workflows, which is a paid Bubble plan feature (Starter $32/month or above). On the free plan, your access token expires after 60 minutes and there is no mechanism to refresh it automatically. The initial token exchange Backend Workflow itself also requires a paid plan — Backend Workflows are not available on Bubble's free tier.

### Why is the learner progress data always showing old completion percentages?

LinkedIn Learning processes learner activity data with a delay of 24–72 hours depending on the data type (course completion vs. time spent). This lag exists on LinkedIn's servers and cannot be reduced by changing how frequently Bubble polls the API. The correct approach is to cache the data and display the last_synced timestamp so users understand they are viewing recent-but-not-live data.

### Can Bubble employees or users outside my organization access my company's LinkedIn Learning data?

Not if you configure Bubble correctly. Your client credentials are stored in Private API Connector headers and never reach the browser. Data stored in Bubble's database is protected by Bubble's privacy rules — configure those rules on your CourseCatalog and LearnerProgress data types to restrict reads to authenticated users only. Bubble's own team has access to the underlying infrastructure, but proper privacy rules prevent data exposure through Bubble's Data API.

### What happens if my LinkedIn developer app credentials are revoked or regenerated?

All API calls will immediately start returning 401 errors. Update the client_id and client_secret in your token exchange Backend Workflow (the 'Make an API request' step's POST parameters), then manually trigger the workflow to generate a fresh token. The API Connector's Authorization header reads dynamically from the stored token, so no changes are needed there — only the token exchange workflow parameters need updating.

---

Source: https://www.rapidevelopers.com/bubble-integrations/lynda-linkedin-learning
© RapidDev — https://www.rapidevelopers.com/bubble-integrations/lynda-linkedin-learning
