To integrate Replit with LinkedIn Ads, create a LinkedIn developer app, complete the OAuth 2.0 flow to obtain an access token with r_ads and rw_ads permissions, store it in Replit Secrets (lock icon 🔒), and call the LinkedIn Marketing API v2 from Python or Node.js server-side code to manage B2B ad campaigns, retrieve performance data, and build audience segments.
Why Connect Replit to LinkedIn Ads?
LinkedIn Ads is the dominant platform for B2B advertising, with targeting capabilities that no other ad network matches — job title, seniority, company size, industry, and skills. The LinkedIn Marketing API gives you programmatic control over your entire LinkedIn advertising operation: creating campaigns, managing budgets, retrieving performance metrics, uploading audience segments, and managing lead gen form responses. Connecting your Replit app to LinkedIn Ads enables automated campaign management, custom reporting dashboards, and CRM data sync workflows.
Common use cases include building executive dashboards that pull LinkedIn ad performance alongside data from other channels, automating the creation of new campaigns based on CRM segment updates, and syncing lead gen form responses directly into your database or CRM without manually downloading from LinkedIn Campaign Manager. The LinkedIn Marketing API v2 provides consistent REST endpoints for all these scenarios.
Replit's Secrets system (lock icon 🔒 in the sidebar) is essential for LinkedIn integrations because OAuth access tokens grant access to your advertising account and its billing information. Store LINKEDIN_ACCESS_TOKEN and LINKEDIN_CLIENT_SECRET in Replit Secrets. LinkedIn access tokens expire after 60 days — implement token refresh logic or use a manual rotation process. Never expose these tokens in client-side code or commit them to Git.
Integration method
You connect Replit to LinkedIn Ads by creating a LinkedIn developer app with marketing permissions, completing an OAuth 2.0 authorization flow to obtain access and refresh tokens, and storing them in Replit Secrets. Your server-side Python or Node.js code calls the LinkedIn Marketing API v2 using the access token in an Authorization: Bearer header. The API covers campaign groups, campaigns, creatives, targeting facets, and analytics for all LinkedIn ad formats.
Prerequisites
- A Replit account with a Python or Node.js project created
- A LinkedIn account with access to LinkedIn Campaign Manager and an active ad account
- A LinkedIn developer app created at developer.linkedin.com with Marketing Developer Platform access
- Basic familiarity with OAuth 2.0 authorization flows
- Node.js 18+ or Python 3.10+ (both available on Replit by default)
Step-by-step guide
Create a LinkedIn Developer App and Request Marketing Permissions
Create a LinkedIn Developer App and Request Marketing Permissions
Go to developer.linkedin.com and click 'Create app'. Fill in the app name, associate it with your LinkedIn Company Page, and provide the required information. Upload a logo and accept the developer agreement. Once the app is created, go to the 'Products' tab. Find 'Marketing Developer Platform' and click 'Request access'. This product grants access to the LinkedIn Marketing API including campaign management and analytics. The request is reviewed by LinkedIn — approval typically takes 1-5 business days for new apps. You will receive an email when access is granted. While waiting for Marketing Developer Platform approval, go to the 'Auth' tab of your app. You will see your Client ID and Client Secret — copy both. Also add a valid OAuth 2.0 redirect URI for your Replit app: https://your-app.replit.app/auth/callback. During development, you can use https://localhost:3000/auth/callback as well. Once approved, your app will have these key OAuth scopes available: r_ads (read campaign data), rw_ads (read and write campaigns), r_ads_reporting (read analytics), and r_basicprofile (read user profile). For most integrations, you need r_ads_reporting for analytics and rw_ads for campaign management.
Pro tip: Request Marketing Developer Platform access as early as possible since LinkedIn's review process takes several business days. Build and test your integration against the sandbox while waiting for approval.
Expected result: Your LinkedIn developer app has Marketing Developer Platform access approved, and you have your Client ID and Client Secret ready.
Complete OAuth 2.0 Authorization and Store Tokens
Complete OAuth 2.0 Authorization and Store Tokens
LinkedIn uses OAuth 2.0 Authorization Code flow for marketing API access. You cannot generate tokens directly from the dashboard — you must complete the OAuth flow to get a token that is linked to your ad account. To complete the OAuth flow, direct a browser to this authorization URL (replacing YOUR_CLIENT_ID and YOUR_REDIRECT_URI): https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&scope=r_ads%20rw_ads%20r_ads_reporting After authorizing, LinkedIn redirects to your callback URL with a code parameter. Exchange this code for an access token by sending a POST request to https://www.linkedin.com/oauth/v2/accessToken with grant_type=authorization_code, your code, client_id, client_secret, and redirect_uri. The response includes an access_token (valid for 60 days) and optionally a refresh_token. Copy the access_token immediately. Store it in Replit Secrets as LINKEDIN_ACCESS_TOKEN along with your LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET, and your LinkedIn Ad Account ID as LINKEDIN_AD_ACCOUNT_ID. Your Ad Account ID (also called Sponsor Account ID) appears in LinkedIn Campaign Manager in the URL as a numeric value after '/urn:li:sponsoredAccount:'. It is typically a 9-10 digit number.
1import os2import requests34# Exchange authorization code for access token5def exchange_code_for_token(auth_code: str, redirect_uri: str) -> dict:6 """Call this once after completing the OAuth flow to get your initial token."""7 token_url = "https://www.linkedin.com/oauth/v2/accessToken"8 data = {9 "grant_type": "authorization_code",10 "code": auth_code,11 "client_id": os.environ["LINKEDIN_CLIENT_ID"],12 "client_secret": os.environ["LINKEDIN_CLIENT_SECRET"],13 "redirect_uri": redirect_uri14 }15 response = requests.post(token_url, data=data)16 response.raise_for_status()17 token_data = response.json()18 print(f"Access token expires in: {token_data.get('expires_in')} seconds")19 print(f"Access token: {token_data['access_token'][:20]}...")20 return token_data2122# After getting the token, store it manually in Replit Secrets:23# LINKEDIN_ACCESS_TOKEN = token_data['access_token']2425if __name__ == "__main__":26 # Replace these with your actual values from the OAuth flow27 AUTH_CODE = input("Enter the authorization code from the redirect URL: ")28 REDIRECT_URI = "https://your-app.replit.app/auth/callback"29 tokens = exchange_code_for_token(AUTH_CODE, REDIRECT_URI)30 print("\nStore this in Replit Secrets as LINKEDIN_ACCESS_TOKEN:")31 print(tokens['access_token'])Pro tip: LinkedIn access tokens expire after 60 days. Set a calendar reminder before expiry to re-run the OAuth flow and update LINKEDIN_ACCESS_TOKEN in Replit Secrets. The LinkedIn API returns a 401 with 'Expired access token' when you need to refresh.
Expected result: You receive an access_token from LinkedIn's OAuth endpoint and store it as LINKEDIN_ACCESS_TOKEN in Replit Secrets.
Retrieve Campaigns and Analytics with Python
Retrieve Campaigns and Analytics with Python
The LinkedIn Marketing API v2 uses REST with JSON responses. Authentication uses Bearer tokens in the Authorization header. The base URL is https://api.linkedin.com/v2/. Many endpoints use URL-encoded URNs for resource IDs — for example, the ad account is referenced as urn:li:sponsoredAccount:{accountId}. Campaign data requires querying sponsoredCampaigns or adCampaignGroups. Analytics data is retrieved from adAnalyticsV2 using a pivot parameter to specify whether to aggregate by CAMPAIGN, CAMPAIGN_GROUP, or CREATIVE. The code below demonstrates fetching all campaigns, retrieving analytics for a date range, and pulling lead gen form responses. Install the requests library if not available.
1import os2import requests3from datetime import datetime, timedelta4from urllib.parse import quote56ACCESS_TOKEN = os.environ["LINKEDIN_ACCESS_TOKEN"]7AD_ACCOUNT_ID = os.environ["LINKEDIN_AD_ACCOUNT_ID"]8BASE_URL = "https://api.linkedin.com/v2"910HEADERS = {11 "Authorization": f"Bearer {ACCESS_TOKEN}",12 "Content-Type": "application/json",13 "X-Restli-Protocol-Version": "2.0.0"14}1516def get_account_info() -> dict:17 """Verify API access and retrieve account details."""18 url = f"{BASE_URL}/adAccountsV2/{AD_ACCOUNT_ID}"19 response = requests.get(url, headers=HEADERS)20 response.raise_for_status()21 return response.json()2223def get_campaign_groups() -> list:24 """Fetch all campaign groups (top-level campaign containers)."""25 url = f"{BASE_URL}/adCampaignGroupsV2"26 params = {27 "q": "search",28 "search.account.values[0]": f"urn:li:sponsoredAccount:{AD_ACCOUNT_ID}",29 "count": 10030 }31 response = requests.get(url, headers=HEADERS, params=params)32 response.raise_for_status()33 return response.json().get('elements', [])3435def get_campaigns(status: str = None) -> list:36 """Fetch all campaigns. Optional status: ACTIVE, PAUSED, ARCHIVED."""37 url = f"{BASE_URL}/adCampaignsV2"38 params = {39 "q": "search",40 "search.account.values[0]": f"urn:li:sponsoredAccount:{AD_ACCOUNT_ID}",41 "count": 10042 }43 if status:44 params["search.status.values[0]"] = status45 response = requests.get(url, headers=HEADERS, params=params)46 response.raise_for_status()47 return response.json().get('elements', [])4849def get_campaign_analytics(start_days_ago: int = 30) -> list:50 """Fetch campaign-level analytics for the past N days."""51 end_date = datetime.now()52 start_date = end_date - timedelta(days=start_days_ago)5354 url = f"{BASE_URL}/adAnalyticsV2"55 params = {56 "q": "analytics",57 "pivot": "CAMPAIGN",58 "dateRange.start.day": start_date.day,59 "dateRange.start.month": start_date.month,60 "dateRange.start.year": start_date.year,61 "dateRange.end.day": end_date.day,62 "dateRange.end.month": end_date.month,63 "dateRange.end.year": end_date.year,64 "timeGranularity": "ALL",65 "accounts[0]": f"urn:li:sponsoredAccount:{AD_ACCOUNT_ID}",66 "fields": "costInLocalCurrency,impressions,clicks,totalEngagements,leads,pivotValue"67 }68 response = requests.get(url, headers=HEADERS, params=params)69 response.raise_for_status()70 return response.json().get('elements', [])7172def get_lead_gen_form_responses(form_id: str, since_days: int = 1) -> list:73 """Fetch lead gen form responses from the past N days."""74 since_ts = int((datetime.now() - timedelta(days=since_days)).timestamp() * 1000)75 url = f"{BASE_URL}/leadGenerationFormResponses"76 params = {77 "q": "account",78 "account": f"urn:li:sponsoredAccount:{AD_ACCOUNT_ID}",79 "form": f"urn:li:leadGenerationForm:{form_id}",80 "submittedAtStart": since_ts,81 "count": 10082 }83 response = requests.get(url, headers=HEADERS, params=params)84 response.raise_for_status()85 return response.json().get('elements', [])8687# Example usage88if __name__ == "__main__":89 try:90 account = get_account_info()91 print(f"Account: {account.get('name', AD_ACCOUNT_ID)}")9293 campaigns = get_campaigns(status='ACTIVE')94 print(f"Active campaigns: {len(campaigns)}")95 for c in campaigns[:3]:96 print(f" [{c.get('id')}] {c.get('name')} — {c.get('status')}")9798 analytics = get_campaign_analytics(30)99 print(f"\nAnalytics data points: {len(analytics)}")100 for stat in analytics[:3]:101 print(f" Campaign: {stat.get('pivotValue')} — "102 f"Spend: {stat.get('costInLocalCurrency', 0):.2f}, "103 f"Clicks: {stat.get('clicks', 0)}")104 except requests.HTTPError as e:105 print(f"API error: {e.response.status_code} — {e.response.text}")Pro tip: LinkedIn API responses use URN format for resource IDs (e.g., 'urn:li:sponsoredCampaign:123456'). When referencing campaigns in analytics queries, use the full URN format. The numeric ID alone is not always accepted.
Expected result: Running the script prints your ad account name, lists active campaigns with their IDs, and shows 30-day analytics data including spend and clicks per campaign.
Build a Node.js Ads Management API
Build a Node.js Ads Management API
For Node.js projects, use axios (npm install axios) to call the LinkedIn Marketing API. The Express server below provides endpoints for fetching campaigns, retrieving analytics, and pausing or resuming campaigns — the core operations for a campaign management dashboard. LinkedIn's API uses some non-standard REST conventions. Campaign status updates use PATCH with a specific body format. The X-Restli-Protocol-Version: 2.0.0 header is required for most endpoints. Collection endpoints use cursor-based pagination via the start and count parameters. For creating new campaigns, the API requires specifying targeting criteria using LinkedIn's facet system. Targeting facets include member skills, job titles (urn:li:title:), job functions (urn:li:jobFunction:), industries (urn:li:industry:), and companies (urn:li:organization:). Facet IDs can be discovered via the /targetingFacets endpoint.
1const express = require('express');2const axios = require('axios');34const app = express();5app.use(express.json());67const ACCESS_TOKEN = process.env.LINKEDIN_ACCESS_TOKEN;8const AD_ACCOUNT_ID = process.env.LINKEDIN_AD_ACCOUNT_ID;9const BASE_URL = 'https://api.linkedin.com/v2';1011const linkedin = axios.create({12 baseURL: BASE_URL,13 headers: {14 'Authorization': `Bearer ${ACCESS_TOKEN}`,15 'Content-Type': 'application/json',16 'X-Restli-Protocol-Version': '2.0.0'17 }18});1920// Get all campaigns for the account21app.get('/campaigns', async (req, res) => {22 try {23 const params = {24 q: 'search',25 'search.account.values[0]': `urn:li:sponsoredAccount:${AD_ACCOUNT_ID}`,26 count: 10027 };28 if (req.query.status) {29 params['search.status.values[0]'] = req.query.status.toUpperCase();30 }31 const response = await linkedin.get('/adCampaignsV2', { params });32 res.json(response.data.elements || []);33 } catch (err) {34 console.error('Campaigns error:', err.response?.data);35 res.status(err.response?.status || 500).json({ error: err.message });36 }37});3839// Get analytics for all campaigns in the account40app.get('/analytics', async (req, res) => {41 const days = parseInt(req.query.days) || 30;42 const endDate = new Date();43 const startDate = new Date(endDate - days * 24 * 60 * 60 * 1000);4445 const formatDate = (d) => ({46 day: d.getDate(),47 month: d.getMonth() + 1,48 year: d.getFullYear()49 });5051 const start = formatDate(startDate);52 const end = formatDate(endDate);5354 try {55 const params = {56 q: 'analytics',57 pivot: 'CAMPAIGN',58 'dateRange.start.day': start.day,59 'dateRange.start.month': start.month,60 'dateRange.start.year': start.year,61 'dateRange.end.day': end.day,62 'dateRange.end.month': end.month,63 'dateRange.end.year': end.year,64 timeGranularity: 'ALL',65 'accounts[0]': `urn:li:sponsoredAccount:${AD_ACCOUNT_ID}`,66 fields: 'costInLocalCurrency,impressions,clicks,leads,pivotValue'67 };68 const response = await linkedin.get('/adAnalyticsV2', { params });69 res.json(response.data.elements || []);70 } catch (err) {71 res.status(err.response?.status || 500).json({ error: err.message });72 }73});7475// Pause or resume a campaign76app.patch('/campaigns/:id/status', async (req, res) => {77 const { status } = req.body; // 'ACTIVE' or 'PAUSED'78 if (!['ACTIVE', 'PAUSED'].includes(status)) {79 return res.status(400).json({ error: 'status must be ACTIVE or PAUSED' });80 }81 try {82 // LinkedIn uses PATCH with full campaign object for status updates83 await linkedin.patch(`/adCampaignsV2/${req.params.id}`, {84 patch: { $set: { status } }85 });86 res.json({ success: true, id: req.params.id, status });87 } catch (err) {88 res.status(err.response?.status || 500).json({ error: err.message });89 }90});9192app.listen(3000, '0.0.0.0', () => {93 console.log('LinkedIn Ads integration server running on port 3000');94});Pro tip: LinkedIn's PATCH endpoint for campaign updates uses a non-standard body format: { patch: { $set: { fieldName: value } } }. Unlike typical REST APIs where you PATCH with a partial object, LinkedIn requires this explicit $set wrapper for field updates.
Expected result: The server starts on port 3000. A GET request to /campaigns returns your LinkedIn ad campaigns, and a GET to /analytics returns spend and click data for the past 30 days.
Handle Token Refresh and Deploy
Handle Token Refresh and Deploy
LinkedIn access tokens expire after 60 days. Unlike some platforms that offer automatic token refresh, LinkedIn requires you to either re-run the OAuth flow or implement a token refresh using a refresh token (available only with specific LinkedIn partnership programs). For most integrations, the practical solution is to build a lightweight token rotation script and schedule it before expiry. For production deployments, consider using LinkedIn's OAuth 2.0 refresh token flow if your app has been approved for it. Otherwise, implement an alert that fires 10 days before token expiry so you can re-authorize manually. For deployment, choose Autoscale in Replit for web apps that display LinkedIn ad dashboards or serve API endpoints queried by a frontend. Choose Reserved VM if you have a background process that pulls LinkedIn analytics data on a schedule and stores it in a database. The Reserved VM ensures no cold-start delay when your scheduled task runs. After deploying, update your LinkedIn developer app's OAuth redirect URI to include your production Replit URL alongside the development URL. Both URIs must be registered in the LinkedIn developer app settings.
1import os2import requests3from datetime import datetime45def check_token_validity() -> dict:6 """7 Check if the current access token is still valid.8 Returns token info including expiry details.9 """10 token = os.environ["LINKEDIN_ACCESS_TOKEN"]11 url = "https://www.linkedin.com/oauth/v2/introspectToken"12 data = {13 "client_id": os.environ["LINKEDIN_CLIENT_ID"],14 "client_secret": os.environ["LINKEDIN_CLIENT_SECRET"],15 "token": token16 }17 response = requests.post(url, data=data)18 response.raise_for_status()19 token_info = response.json()2021 if token_info.get('active'):22 expires_at = token_info.get('expires_at', 0)23 if expires_at:24 expiry_date = datetime.fromtimestamp(expires_at / 1000)25 days_left = (expiry_date - datetime.now()).days26 print(f"Token is active. Expires in {days_left} days ({expiry_date.date()})")27 if days_left < 10:28 print("WARNING: Token expires soon — re-authorize the LinkedIn OAuth flow!")29 else:30 print("Token is INACTIVE or EXPIRED. Re-authorization required.")3132 return token_info3334if __name__ == "__main__":35 info = check_token_validity()36 print(f"Token status: {'Active' if info.get('active') else 'Inactive'}")Pro tip: Add a GET /health endpoint to your deployed Replit server that calls check_token_validity() and returns the token expiry date. Set up a monitoring alert (or a scheduled Replit job) to hit this endpoint weekly so you get advance warning before the token expires.
Expected result: Running check_token.py prints the current token's active status and the number of days until it expires, warning you if renewal is needed soon.
Common use cases
B2B Campaign Performance Dashboard
Build a Replit web app that pulls daily spend, impressions, clicks, and lead counts from all active LinkedIn campaigns and displays them alongside Google Analytics and CRM data in a unified marketing dashboard. Account managers can see cross-channel performance without logging into multiple platforms.
Build a Flask app with a /linkedin/performance endpoint that fetches all active LinkedIn campaigns from the Marketing API using LINKEDIN_ACCESS_TOKEN, retrieves spend and click analytics for the past 30 days per campaign, and returns a JSON array sorted by cost-per-click.
Copy this prompt to try it in Replit
Matched Audience Upload for Account-Based Marketing
Sync your CRM's target account list to LinkedIn Matched Audiences. When your sales team updates the list of target companies in your CRM, your Replit backend extracts the company names and employee emails, uploads them to LinkedIn via the Audience API, and creates a company-targeted campaign to reach decision makers at those specific accounts.
Write a Python script that reads a list of target company names from a PostgreSQL database, creates a LinkedIn Matched Audience using the company list via the /matched-audiences endpoint, and returns the audience ID for use in campaign targeting.
Copy this prompt to try it in Replit
Lead Gen Form Response Sync
Automatically pull LinkedIn Lead Gen Form responses into your database or CRM every hour using a Replit scheduled job. When a prospect fills out a LinkedIn lead form, their contact details are stored in LinkedIn Campaign Manager — your Replit job retrieves new responses and pushes them to HubSpot or your database so sales reps get them immediately.
Create a Python script that fetches LinkedIn Lead Gen Form responses submitted in the last hour using the /leadGenerationForms API, deduplicates against a PostgreSQL leads table, and inserts new leads with their name, email, job title, and company.
Copy this prompt to try it in Replit
Troubleshooting
API returns 401 with 'Expired access token' or 'Invalid access token'
Cause: LinkedIn access tokens expire after 60 days. If you see this error, the token stored in LINKEDIN_ACCESS_TOKEN in Replit Secrets is either expired or was generated with an insufficient OAuth scope.
Solution: Re-run the OAuth 2.0 authorization flow to generate a new access token. Navigate to the authorization URL with the required scopes, complete the LinkedIn login, exchange the authorization code for a new token, and update LINKEDIN_ACCESS_TOKEN in Replit Secrets. Run check_token.py to verify the new token is active.
1# Python: build the authorization URL for re-authentication2from urllib.parse import urlencode34params = {5 'response_type': 'code',6 'client_id': os.environ['LINKEDIN_CLIENT_ID'],7 'redirect_uri': 'https://your-app.replit.app/auth/callback',8 'scope': 'r_ads rw_ads r_ads_reporting'9}10auth_url = 'https://www.linkedin.com/oauth/v2/authorization?' + urlencode(params)11print('Go to this URL to re-authorize:', auth_url)API returns 403 — 'Not enough permissions to access: GET /adCampaignsV2'
Cause: The access token was generated without the required OAuth scopes (r_ads or rw_ads), or the Marketing Developer Platform product has not been approved for your LinkedIn developer app.
Solution: Check the Products tab of your LinkedIn developer app at developer.linkedin.com. If Marketing Developer Platform shows 'Requested' rather than 'Approved', wait for LinkedIn to review and approve the access request. Once approved, re-run the OAuth flow and request the r_ads and rw_ads scopes explicitly in the authorization URL.
Analytics endpoint returns empty elements array despite active campaigns
Cause: The date range in the analytics query does not overlap with the campaign's active period, the campaign has not generated any activity in that period, or the timeGranularity does not match the date range scope.
Solution: Try a broader date range and use timeGranularity: ALL for account-level totals rather than DAILY when the date range spans multiple months. Also verify your campaigns are set to ACTIVE (not PAUSED) and have received impressions by checking LinkedIn Campaign Manager directly.
1# Python: verify campaigns have data by checking Campaign Manager status2campaigns = get_campaigns(status='ACTIVE')3print(f"Active campaigns: {len(campaigns)}")4if not campaigns:5 print("No active campaigns — pause status or check account billing")PATCH campaign status returns 422 — 'Invalid input'
Cause: LinkedIn's PATCH endpoint requires a non-standard body format with a $set wrapper. Sending a plain JSON object with the status field (like most REST APIs expect) will return a 422 Unprocessable Entity error.
Solution: Use the LinkedIn-specific PATCH body format: { 'patch': { '$set': { 'status': 'PAUSED' } } }. This wraps the updated fields in a $set operator that LinkedIn's API requires for partial updates.
1# Python: correct PATCH format for LinkedIn2response = requests.patch(3 f"{BASE_URL}/adCampaignsV2/{campaign_id}",4 json={'patch': {'$set': {'status': 'PAUSED'}}},5 headers=HEADERS6)7response.raise_for_status()Best practices
- Store LINKEDIN_ACCESS_TOKEN, LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET, and LINKEDIN_AD_ACCOUNT_ID in Replit Secrets (lock icon 🔒) — never in source code or Git.
- Always include the X-Restli-Protocol-Version: 2.0.0 header in requests — omitting it causes inconsistent behavior across LinkedIn API endpoints.
- Set a calendar reminder 10 days before your 60-day access token expires to re-run the OAuth flow and update the token in Replit Secrets.
- Request Marketing Developer Platform access early — LinkedIn's review process takes 1-5 business days and is required before you can use the marketing APIs.
- Always create new campaigns with status PAUSED in code to prevent accidental spend during development and testing.
- Use URN format for resource identifiers in analytics queries (urn:li:sponsoredAccount:ID) as plain numeric IDs are not always accepted by all endpoints.
- Deploy with Autoscale for ad dashboards and user-facing tools; use Reserved VM for scheduled jobs that pull daily analytics and store them in a database.
- Implement exponential backoff for API calls — LinkedIn rate limits vary by endpoint, and rate limit responses (429) should be retried with increasing delays.
Alternatives
Facebook Ads provides broader B2C audience reach with demographic and interest targeting, making it a better fit when targeting consumers rather than professional audiences by job title and company.
Quora Ads targets users based on question-and-answer context and topic interests, making it a complementary channel for reaching technically curious audiences with lower CPC than LinkedIn.
Pinterest Ads targets high purchase-intent audiences through visual product discovery, making it more effective for e-commerce and consumer brands than for B2B professional services.
Frequently asked questions
How do I store my LinkedIn access token in Replit?
Click the lock icon 🔒 in the left sidebar of your Replit project to open the Secrets pane. Add LINKEDIN_ACCESS_TOKEN with your OAuth access token, LINKEDIN_AD_ACCOUNT_ID with your numeric sponsor account ID, and LINKEDIN_CLIENT_ID and LINKEDIN_CLIENT_SECRET from your LinkedIn developer app. Access them in Python with os.environ['LINKEDIN_ACCESS_TOKEN'] and in Node.js with process.env.LINKEDIN_ACCESS_TOKEN.
How long does the LinkedIn access token last?
LinkedIn OAuth access tokens expire after 60 days. There is no automatic refresh unless your app has been approved for the offline access scope (available only to approved LinkedIn Marketing Partners). For most developers, the solution is to set a calendar reminder before expiry and re-run the OAuth authorization flow to generate a new token, then update LINKEDIN_ACCESS_TOKEN in Replit Secrets.
Do I need Marketing Developer Platform approval to use the LinkedIn Ads API?
Yes. The LinkedIn Marketing Developer Platform product must be approved for your LinkedIn developer app before you can access the Ads API. Request access from the Products tab of your app at developer.linkedin.com. LinkedIn reviews requests within 1-5 business days. Without this approval, your requests will return 403 permission errors even with valid OAuth tokens.
What is my LinkedIn Ad Account ID and where do I find it?
Your LinkedIn Ad Account ID (also called Sponsor Account ID) is a 9-10 digit number visible in LinkedIn Campaign Manager. When you are in Campaign Manager, the URL contains the segment '/urn:li:sponsoredAccount:XXXXXXXXX' — the numeric portion is your account ID. You can also find it in Campaign Manager under Account > Settings. Store it as LINKEDIN_AD_ACCOUNT_ID in Replit Secrets.
Can I create LinkedIn ad campaigns programmatically from Replit?
Yes. The LinkedIn Marketing API supports full campaign creation including campaign groups, campaigns, ad creatives, and targeting. You need rw_ads OAuth scope and Marketing Developer Platform approval. Create campaigns with status PAUSED initially, configure all targeting and creative settings, then activate via a separate status update call to prevent accidental spend during testing.
Why does the LinkedIn PATCH endpoint return a 422 error?
LinkedIn's PATCH endpoint uses a non-standard body format that requires wrapping updated fields in a '$set' operator: { 'patch': { '$set': { 'status': 'PAUSED' } } }. Sending a plain JSON object with just the fields you want to update (as with typical REST APIs) returns a 422 Unprocessable Entity error. Always use this $set wrapper format for LinkedIn PATCH requests.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation