Skip to main content
RapidDev - Software Development Agency
bubble-integrationsBubble API Connector

LinkedIn Ads

Connect Bubble to LinkedIn Marketing API v2 using OAuth 2.0 with the mandatory LinkedIn-Version header and Private Bearer token in the API Connector. The two tricky parts — URL-encoding URN entity references in Bubble expressions and refreshing 60-day OAuth tokens via a Backend Workflow — separate working integrations from broken dashboards.

What you'll learn

  • How to configure all four required LinkedIn Marketing API headers in Bubble's API Connector
  • How to URL-encode LinkedIn URN entity references using Bubble's :URL encode operator in dynamic expressions
  • How to fetch ad accounts, campaign lists, and analytics from LinkedIn Marketing API v2 endpoints
  • How to pull LinkedIn Lead Gen Form submissions into your Bubble app for CRM-style workflows
  • How to build a recurring Backend Workflow to refresh the 60-day OAuth token before it expires
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate15 min read2–3 hoursMarketingLast updated July 2026RapidDev Engineering Team
TL;DR

Connect Bubble to LinkedIn Marketing API v2 using OAuth 2.0 with the mandatory LinkedIn-Version header and Private Bearer token in the API Connector. The two tricky parts — URL-encoding URN entity references in Bubble expressions and refreshing 60-day OAuth tokens via a Backend Workflow — separate working integrations from broken dashboards.

Quick facts about this guide
FactValue
ToolLinkedIn Ads
CategoryMarketing
MethodBubble API Connector
DifficultyIntermediate
Time required2–3 hours
Last updatedJuly 2026

Build a LinkedIn Ads Analytics Dashboard in Bubble

LinkedIn Ads is the primary B2B advertising channel for companies targeting professionals by job function, seniority, company size, and industry. Connecting Bubble to LinkedIn Marketing API v2 lets you pull campaign analytics, monitor spend and lead volume, and surface Lead Gen Form submissions — all without requiring your team to log into LinkedIn Campaign Manager repeatedly. The integration has two technical differentiators from other ad platforms. First: LinkedIn's API requires a LinkedIn-Version header on every request, formatted as a 6-digit year-month string (e.g., 202401 for the January 2024 API version). Omitting this header returns a 400 error with a vague message. Second: LinkedIn uses a URN notation for all entity references — your ad account isn't identified by a plain number but by a string like urn:li:sponsoredAccount:123456. When passing this as a URL query parameter, it must be percent-encoded. In Bubble's dynamic expressions, this means using the :URL encode operator on any expression containing a URN. Get both of these right, and the rest of the integration is standard API Connector work. The third thing to plan for early is token expiry: LinkedIn OAuth tokens last 60 days, which is shorter than most platforms. Build the token refresh Backend Workflow before launch.

Integration method

Bubble API Connector

Connect to LinkedIn Marketing API v2 with OAuth 2.0 Bearer token stored as a Private header, alongside mandatory LinkedIn-Version and X-Restli-Protocol-Version headers.

Prerequisites

  • A LinkedIn Ads account with active or historical campaigns
  • A LinkedIn Developer App created at developer.linkedin.com with the Marketing Developer Platform product requested and approved
  • OAuth 2.0 access token with r_ads and r_ads_reporting scopes (add rw_ads for write operations) obtained through the 3-legged Authorization Code flow
  • A Bubble app on any plan for initial setup (paid plan required for scheduled Backend Workflow token refresh)

Step-by-step guide

1

Create Your LinkedIn Developer App and Request Marketing Developer Platform Access

Go to developer.linkedin.com and click Create app. Provide your app name (e.g., 'Bubble Ads Dashboard'), associate it with a LinkedIn Company Page (required for Marketing API), and upload a logo. After creating the app, navigate to the Products tab within your Developer App and find 'Marketing Developer Platform'. Click Request access. LinkedIn reviews these requests manually — your own ad account will be automatically added to the app's allowed accounts in Development mode, so you can test immediately. Wait for the approval email before using production-level scopes with other accounts. While waiting, note your App Client ID and App Client Secret from the Auth tab — you will need these for the OAuth flow. Also confirm which scopes to request: r_ads (read ad accounts and campaigns), r_ads_reporting (read analytics), and optionally rw_ads (create and modify campaigns) if you need write access. These scopes are configured during the OAuth authorization URL construction in Step 2.

Pro tip: The Marketing Developer Platform product approval is separate from app creation. You can test with your own LinkedIn Ads account immediately in Development mode while awaiting approval for production partner access.

Expected result: A LinkedIn Developer App exists with your Client ID and Secret. The Marketing Developer Platform product request is submitted. You can proceed with the OAuth flow for your own ad account while awaiting broader approval.

2

Complete the OAuth 2.0 Authorization Code Flow and Obtain Your Bearer Token

LinkedIn uses a 3-legged OAuth 2.0 Authorization Code flow, meaning you (or your users) must explicitly authorize the application. Construct the authorization URL as follows: https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&scope=r_ads%20r_ads_reporting&state=RANDOM_STRING. Open this URL in your browser while logged into your LinkedIn account that has access to the ad accounts you want to query. After authorizing, LinkedIn redirects to YOUR_REDIRECT_URI with a code= query parameter. Copy that authorization code — it expires in a few minutes. Exchange it for tokens: send a POST request to https://www.linkedin.com/oauth/v2/accessToken with Content-Type: application/x-www-form-urlencoded and body parameters grant_type=authorization_code, code=YOUR_AUTH_CODE, redirect_uri=YOUR_REDIRECT_URI, client_id=YOUR_CLIENT_ID, client_secret=YOUR_CLIENT_SECRET. The response includes an access_token (valid 60 days) and a refresh_token (valid longer). Store both in a Bubble App Config Data Type — the access token goes into the API Connector group header now, and the refresh token is kept in your database for the automated renewal workflow in Step 6.

linkedin-oauth-flow.txt
1// Step 1: Direct user (or yourself) to authorization URL:
2// https://www.linkedin.com/oauth/v2/authorization
3// ?response_type=code
4// &client_id=YOUR_CLIENT_ID
5// &redirect_uri=https://yourapp.bubbleapps.io/linkedin-callback
6// &scope=r_ads%20r_ads_reporting
7// &state=random_string_123
8
9// Step 2: Exchange authorization code for tokens
10// POST https://www.linkedin.com/oauth/v2/accessToken
11// Content-Type: application/x-www-form-urlencoded
12
13grant_type=authorization_code
14&code=THE_CODE_FROM_REDIRECT
15&redirect_uri=https://yourapp.bubbleapps.io/linkedin-callback
16&client_id=YOUR_CLIENT_ID
17&client_secret=YOUR_CLIENT_SECRET
18
19// Response:
20{
21 "access_token": "AQX...",
22 "expires_in": 5184000,
23 "refresh_token": "AQW...",
24 "refresh_token_expires_in": 31536000
25}

Pro tip: 5,184,000 seconds = 60 days. The expires_in field confirms the 60-day window. Plan your token refresh Backend Workflow to run every 50 days to give a comfortable buffer before expiry.

Expected result: You have a LinkedIn access_token (starting with 'AQX') and a refresh_token. Both are stored in your Bubble App Config Data Type. The access token is ready to configure in the API Connector.

3

Configure the API Connector with All Four Required LinkedIn Headers

In your Bubble editor, open the Plugins tab and click Add plugins. Search for API Connector (by Bubble) and install it. Click API Connector, then Add another API. Name the group LinkedIn Ads. Set the base URL to https://api.linkedin.com. LinkedIn Marketing API v2 requires four headers at the group level. Add them one by one: First — key Authorization, value Bearer YOUR_ACCESS_TOKEN, check Private. This keeps your OAuth token server-side and never exposes it to the browser. Second — key LinkedIn-Version, value 202401 (or the current YYYYMM version; check developer.linkedin.com for the latest stable version). This is mandatory — omitting it returns a 400 error. Third — key X-Restli-Protocol-Version, value 2.0.0. This is required by LinkedIn's REST.li framework. Fourth — key Content-Type, value application/json. Only the Authorization header needs to be Private; the others are non-sensitive and can remain visible. Click Expand to add your first API call.

linkedin-api-connector-headers.json
1// API Connector group: LinkedIn Ads
2// Base URL: https://api.linkedin.com
3
4// Shared headers (add all four):
5{
6 "Authorization": "Bearer AQX_YOUR_ACCESS_TOKEN", // <-- mark Private
7 "LinkedIn-Version": "202401", // <-- mandatory, not Private
8 "X-Restli-Protocol-Version": "2.0.0", // <-- mandatory, not Private
9 "Content-Type": "application/json" // <-- not Private
10}

Pro tip: If you see a 400 Bad Request from LinkedIn with a message like 'Invalid API version' or 'Missing required header', the LinkedIn-Version header is either absent or formatted incorrectly. It must be exactly 6 digits: YYYYMM with no separators (202401, not 2024-01 or 2024/01).

Expected result: The API Connector group 'LinkedIn Ads' has all four required headers configured. The Authorization header is marked Private. The group base URL is https://api.linkedin.com. You are ready to add API calls.

4

Add the Ad Accounts Call and Understand URN Encoding

Add a call named Get Ad Accounts. Set the method to GET and the endpoint to /v2/adAccountsV2 with the URL parameter q=search. Click Initialize call — Bubble makes a live request to LinkedIn. When initialization succeeds, Bubble detects the response fields including the account ID (a plain number like 123456) and the account URN (urn:li:sponsoredAccount:123456). LinkedIn uses URNs as the canonical identifier for every entity. Here is the critical concept that breaks most first-timer Bubble integrations: when you pass a URN as a URL parameter value (as required by analytics and campaign calls), you must URL-encode it. The colon characters in urn:li:sponsoredAccount:123456 become %3A, producing urn%3Ali%3AsponsoredAccount%3A123456. In Bubble, this is handled automatically by using the :URL encode operator on your dynamic expression. For example, if you store the account URN in a text field and bind it to the search.account.values[0] URL parameter, your Bubble dynamic expression should read: Input AdAccount URN's value :URL encode. Failing to URL-encode the URN returns a 400 with no clear error message, or an empty result set — one of the most common LinkedIn Ads integration failures in Bubble.

linkedin-ad-accounts-call.json
1// GET /v2/adAccountsV2
2// Full URL: https://api.linkedin.com/v2/adAccountsV2?q=search
3
4// Expected response:
5{
6 "elements": [
7 {
8 "id": 123456,
9 "name": "My Company Ads",
10 "status": "ACTIVE",
11 "currency": "USD",
12 "$URN": "urn:li:sponsoredAccount:123456"
13 }
14 ]
15}
16
17// URN encoding example:
18// Raw: urn:li:sponsoredAccount:123456
19// Encoded: urn%3Ali%3AsponsoredAccount%3A123456
20
21// In Bubble dynamic expression:
22// [Input AdAccount URN's value :URL encode]

Pro tip: After initializing the ad accounts call, store the returned account URN (e.g., urn:li:sponsoredAccount:123456) in a Bubble Custom State or Data Type field. You'll need to pass it URL-encoded in every subsequent campaign and analytics call.

Expected result: Bubble shows the ad accounts response with elements including account IDs and URNs. You understand how to use :URL encode in Bubble expressions to properly encode LinkedIn URNs for query parameter use.

5

Add Campaign List and Analytics Calls with URL-Encoded URN Parameters

Add a second call named Get Campaigns. Set the method to GET and endpoint to /v2/adCampaignsV2. Add the URL parameter search.account.values[0] and bind its value to your stored account URN with :URL encode applied. Also add q=search as a URL parameter. Initialize this call — Bubble detects campaign fields including id, name, status, dailyBudget, and campaign URNs. Add a third call named Get Campaign Analytics. Set the method to GET and endpoint to /v2/adAnalyticsV2. Add these URL parameters: pivots[0]=CAMPAIGN (the grouping dimension), dateRange.start.day, dateRange.start.month, dateRange.start.year, dateRange.end.day, dateRange.end.month, dateRange.end.year, and campaigns[0] set to the URL-encoded campaign URN. LinkedIn's analytics endpoint splits date range into separate day, month, and year fields — not a single date string. In your Bubble workflow, bind each date part separately: for a date picker input, use :extract day, :extract month, :extract year operators. Initialize the analytics call with hardcoded test date values (e.g., dateRange.start.day=1, month=1, year=2024, end day=31, month=1, year=2024) and a known campaign URN. When initialize succeeds, bind the results — spend, costInLocalCurrency, clicks, impressions — to a Repeating Group displaying your campaign analytics. For lead gen form integrations, add a fourth call: GET /v2/leadGenerationForms/{formUrn}/submissions using the form's URN from LinkedIn Campaign Manager.

linkedin-campaigns-analytics.json
1// GET /v2/adCampaignsV2
2// URL params:
3// q=search
4// search.account.values[0]=urn%3Ali%3AsponsoredAccount%3A123456 <-- URL-encoded!
5
6// GET /v2/adAnalyticsV2
7// URL params:
8// pivots[0]=CAMPAIGN
9// dateRange.start.day=1
10// dateRange.start.month=1
11// dateRange.start.year=2024
12// dateRange.end.day=31
13// dateRange.end.month=1
14// dateRange.end.year=2024
15// campaigns[0]=urn%3Ali%3AsponsoredCampaign%3A987654 <-- URL-encoded campaign URN!
16
17// Expected analytics response:
18{
19 "elements": [
20 {
21 "pivotValue": "urn:li:sponsoredCampaign:987654",
22 "dateRange": { "start": {...}, "end": {...} },
23 "costInLocalCurrency": "245.50",
24 "clicks": 312,
25 "impressions": 18420
26 }
27 ]
28}

Pro tip: LinkedIn's adAnalyticsV2 returns costInLocalCurrency as a string, not a number. In Bubble, use the :converted to number operator to turn it into a numeric value before doing calculations like CPC or ROAS.

Expected result: You have three API Connector calls configured: ad accounts, campaign list, and analytics. Your Bubble Repeating Group shows LinkedIn campaign spend, clicks, and impressions for the selected date range. URNs are correctly URL-encoded in all query parameters.

6

Build a 60-Day Token Refresh Backend Workflow

LinkedIn OAuth tokens expire in 60 days — shorter than most ad platforms. For a production Bubble dashboard, automated token refresh is essential. On a paid Bubble plan, go to Settings → API and enable 'This app exposes a Workflow API'. Navigate to Backend Workflows and create a new workflow named 'Refresh LinkedIn Token'. In your API Connector, add a new call named Refresh Token to a separate group (or the LinkedIn Ads group), set to POST at https://www.linkedin.com/oauth/v2/accessToken with Content-Type: application/x-www-form-urlencoded (set at the call level, overriding the group's application/json Content-Type). Add body parameters: grant_type=refresh_token and refresh_token=YOUR_STORED_REFRESH_TOKEN (dynamic, from App Config). In the Backend Workflow, add two actions: (1) Make API call → Refresh Token, passing the stored refresh_token; (2) Make changes to Thing → App Config record, setting the access_token field to the new access_token from the response. Set up a recurring event to run this workflow every 50 days — 10 days before the 60-day expiry — giving you a buffer if the refresh fails. Update your API Connector Authorization header to pull its value dynamically from the App Config's access_token field rather than a hardcoded string. If you need help structuring this refresh loop alongside the campaign analytics dashboard, RapidDev has built hundreds of Bubble integrations — free scoping call at rapidevelopers.com/contact.

linkedin-token-refresh-workflow.txt
1// POST https://www.linkedin.com/oauth/v2/accessToken
2// Content-Type: application/x-www-form-urlencoded
3// (Override at call level — group Content-Type is application/json)
4
5// Body (form-encoded, use dynamic values in Bubble):
6grant_type=refresh_token
7&refresh_token=AQW_YOUR_STORED_REFRESH_TOKEN
8&client_id=YOUR_CLIENT_ID
9&client_secret=YOUR_CLIENT_SECRET
10
11// Response:
12{
13 "access_token": "AQX_NEW_TOKEN_HERE",
14 "expires_in": 5184000
15}
16
17// Backend Workflow: Refresh LinkedIn Token
18// Trigger: Recurring event every 50 days
19// Action 1: Make API call → Refresh Token
20// Action 2: Make changes to App Config → access_token = Result Step 1's access_token

Pro tip: The token refresh endpoint requires a form-encoded body (application/x-www-form-urlencoded), but your API Connector group Content-Type is application/json. Override the Content-Type to application/x-www-form-urlencoded at the call level for the refresh call only. In Bubble, use the call-level header override option.

Expected result: A recurring Backend Workflow runs every 50 days, calls LinkedIn's token refresh endpoint, and updates the access_token in your App Config Data Type. Your LinkedIn Ads dashboard remains authorized indefinitely without requiring manual re-authentication every two months.

Common use cases

LinkedIn Ads Campaign Performance Dashboard

Pull LinkedIn campaign spend, impressions, clicks, and conversions by date range into a Bubble dashboard. Display CTR, CPC, and cost-per-lead alongside campaign names and status. Filter by date range and campaign objective. Ideal for B2B marketing teams monitoring LinkedIn alongside other channels without switching between platforms.

Bubble Prompt

Build a Bubble LinkedIn Ads analytics dashboard showing campaign-level spend, clicks, impressions, CTR, and CPC for a chosen date range with date picker inputs and a sortable Repeating Group table.

Copy this prompt to try it in Bubble

LinkedIn Lead Gen Form Submissions to Bubble CRM

Automatically pull LinkedIn Lead Gen Form submissions into Bubble's database to populate a CRM-style contact list. When a prospect fills out a LinkedIn lead form, a recurring Bubble Backend Workflow fetches new submissions and creates contact records in Bubble's data types — saving sales teams from manual data export from LinkedIn Campaign Manager.

Bubble Prompt

Create a Bubble workflow that fetches new LinkedIn Lead Gen Form submissions every hour, creates a Contact record in Bubble for each new lead with name, email, company, and job title, and sends a Slack notification to the sales team.

Copy this prompt to try it in Bubble

B2B Ads Comparison Dashboard Across Channels

Combine LinkedIn Ads analytics with Google Ads and HubSpot Marketing Hub data in a single Bubble interface. Show cost-per-lead and pipeline contribution across channels so demand generation teams can compare LinkedIn's higher CPCs against its lead quality and pipeline influence.

Bubble Prompt

Build a Bubble multi-channel reporting page that shows LinkedIn Ads, Google Ads, and HubSpot form submission data side-by-side with unified CPA metrics and a shared date range filter.

Copy this prompt to try it in Bubble

Troubleshooting

API returns 400 Bad Request with a vague error message immediately after setup

Cause: The LinkedIn-Version header is missing or formatted incorrectly. This is the most common LinkedIn Marketing API setup failure. Every request must include LinkedIn-Version as a 6-digit YYYYMM string.

Solution: Verify the LinkedIn-Version header is present in your API Connector group's shared headers and is formatted as YYYYMM (e.g., 202401, not 2024-01 or just 2024). Also ensure X-Restli-Protocol-Version: 2.0.0 is present. Check LinkedIn's developer documentation for the current recommended version value.

Campaign analytics call returns 400 or an empty elements array despite the ad account and campaigns existing

Cause: URN values used as query parameter values are not URL-encoded. LinkedIn requires URN strings (which contain colons) to be percent-encoded when used as query parameter values. urn:li:sponsoredAccount:123456 must become urn%3Ali%3AsponsoredAccount%3A123456.

Solution: In your Bubble dynamic expression for any parameter containing a LinkedIn URN (ad account URN, campaign URN, form URN), add the :URL encode operator. For example: Current cell's campaign URN :URL encode. Rebuild the API call with the correctly encoded parameter and run the Initialize call again.

Initialize call shows 'There was an issue setting up your call' with no field schema detected

Cause: The API Connector Initialize call requires a real, successful response to detect field schema. This error occurs when the request returns a non-200 HTTP status, which prevents Bubble from reading the response body.

Solution: Check that all four headers are set (Authorization Private with Bearer token, LinkedIn-Version, X-Restli-Protocol-Version, Content-Type). Verify the access token has not expired (60-day lifespan). For the ad accounts call, ensure the q=search URL parameter is included — without it, LinkedIn returns a 400.

The dashboard stops working after about two months with 401 Unauthorized errors

Cause: LinkedIn OAuth access tokens expire after 60 days. Without an automated refresh workflow, the token silently expires and all API calls begin returning 401.

Solution: Build the token refresh Backend Workflow described in Step 6, using your stored refresh_token to obtain a new access_token. Run it on a recurring schedule of every 50 days. This requires a paid Bubble plan. If the refresh_token itself has also expired, you must re-run the full OAuth Authorization Code flow from Step 2.

Best practices

  • Always include both the LinkedIn-Version and X-Restli-Protocol-Version headers in your API Connector group. Missing either one causes 400 errors that are difficult to diagnose. Keep the LinkedIn-Version value updated when LinkedIn deprecates older versions.
  • Always mark the Authorization header Private. LinkedIn access tokens grant access to all campaign management and analytics data — exposure through a client-side call would allow anyone to read or modify your ad campaigns.
  • URL-encode all LinkedIn URN values when using them as query parameter values in Bubble. Use the :URL encode dynamic expression operator. Forgetting this is the single most common cause of empty analytics results in LinkedIn Bubble integrations.
  • Store both the access_token and refresh_token in a Bubble App Config Data Type and reference them dynamically in API Connector headers. Never hardcode tokens directly in the API Connector if you plan to automate refresh.
  • Build the 60-day token refresh Backend Workflow before going live with any users. LinkedIn's shorter-than-average token lifespan means even a two-month-old integration will silently break without automation.
  • Split the date range into day, month, and year components when passing to the adAnalyticsV2 endpoint — LinkedIn does not accept a single date string. Use Bubble's :extract day, :extract month, and :extract year operators on your date picker values.
  • Apply Data tab Privacy rules to any Bubble Data Type that stores LinkedIn campaign data or lead gen form submissions. Campaign spend data and prospect contact information are both business-sensitive and should be restricted to admin users only.

Alternatives

Frequently asked questions

Why does my LinkedIn API call return 400 Bad Request immediately after I configure it?

The most likely cause is a missing or incorrectly formatted LinkedIn-Version header. LinkedIn requires this header on every request, formatted as a 6-digit YYYYMM string (e.g., 202401). The second most common cause is missing the X-Restli-Protocol-Version: 2.0.0 header. Confirm both are present in your API Connector group's shared headers and retry the Initialize call.

What is a LinkedIn URN and why do I need to URL-encode it?

LinkedIn uses URN (Uniform Resource Name) notation as entity identifiers throughout its API. For example, an ad account is identified as urn:li:sponsoredAccount:123456. When this URN is passed as a URL query parameter value, the colons in the URN must be percent-encoded because colons have special meaning in URLs. In Bubble, apply the :URL encode operator to any dynamic expression containing a LinkedIn URN before binding it to an API parameter.

How long does my LinkedIn OAuth token last and what happens when it expires?

LinkedIn OAuth access tokens expire after 60 days (5,184,000 seconds as shown in the expires_in field of the token response). When the token expires, all API calls begin returning 401 Unauthorized and your Bubble dashboard stops displaying data. Build a recurring Backend Workflow (paid Bubble plan) that calls LinkedIn's token refresh endpoint every 50 days to stay ahead of the expiry.

Can I use this integration on Bubble's Free plan?

You can configure the API Connector, add all four headers, and build the campaign analytics UI on Bubble's Free plan. However, the scheduled Backend Workflow for automated token refresh requires a paid plan. On Free, your access token will expire after 60 days and require you to manually re-run the OAuth flow. For any production dashboard, a paid Bubble plan is recommended.

How do I get LinkedIn Lead Gen Form submissions into Bubble?

Add a fourth API Connector call to the GET /v2/leadGenerationForms/{formUrn}/submissions endpoint, passing the form URN URL-encoded. The form URN is available from LinkedIn Campaign Manager under your Lead Gen Form's details. Schedule a Backend Workflow to call this endpoint periodically and create Bubble database records for each new submission — effectively building a lightweight CRM synced from LinkedIn lead forms.

RapidDev

Talk to an Expert

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

Book a free consultation

Integrations are where projects stall

We wire Bubble integrations like this one every week — auth, webhooks, edge cases included. Fixed-price builds from $13K, delivered in 6–10 weeks.

Talk to an integration engineer

We put the rapid in RapidDev

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