Skip to main content
RapidDev - Software Development Agency
replit-integrationsStandard API Integration

How to Integrate Replit with LinkedIn Ads

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.

What you'll learn

  • How to create a LinkedIn developer app and obtain OAuth 2.0 access tokens with ads permissions
  • How to store LinkedIn credentials securely in Replit Secrets
  • How to retrieve campaign performance analytics using Python and Node.js
  • How to create and manage LinkedIn ad campaigns and campaign groups via the Marketing API
  • How to handle OAuth token refresh and rate limiting for production LinkedIn Ads integrations
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate16 min read45 minutesMarketingMarch 2026RapidDev Engineering Team
TL;DR

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

Standard API Integration

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

1

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.

2

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.

get_token.py
1import os
2import requests
3
4# Exchange authorization code for access token
5def 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_uri
14 }
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_data
21
22# After getting the token, store it manually in Replit Secrets:
23# LINKEDIN_ACCESS_TOKEN = token_data['access_token']
24
25if __name__ == "__main__":
26 # Replace these with your actual values from the OAuth flow
27 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.

3

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.

linkedin_ads_client.py
1import os
2import requests
3from datetime import datetime, timedelta
4from urllib.parse import quote
5
6ACCESS_TOKEN = os.environ["LINKEDIN_ACCESS_TOKEN"]
7AD_ACCOUNT_ID = os.environ["LINKEDIN_AD_ACCOUNT_ID"]
8BASE_URL = "https://api.linkedin.com/v2"
9
10HEADERS = {
11 "Authorization": f"Bearer {ACCESS_TOKEN}",
12 "Content-Type": "application/json",
13 "X-Restli-Protocol-Version": "2.0.0"
14}
15
16def 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()
22
23def 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": 100
30 }
31 response = requests.get(url, headers=HEADERS, params=params)
32 response.raise_for_status()
33 return response.json().get('elements', [])
34
35def 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": 100
42 }
43 if status:
44 params["search.status.values[0]"] = status
45 response = requests.get(url, headers=HEADERS, params=params)
46 response.raise_for_status()
47 return response.json().get('elements', [])
48
49def 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)
53
54 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', [])
71
72def 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": 100
82 }
83 response = requests.get(url, headers=HEADERS, params=params)
84 response.raise_for_status()
85 return response.json().get('elements', [])
86
87# Example usage
88if __name__ == "__main__":
89 try:
90 account = get_account_info()
91 print(f"Account: {account.get('name', AD_ACCOUNT_ID)}")
92
93 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')}")
97
98 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.

4

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.

server.js
1const express = require('express');
2const axios = require('axios');
3
4const app = express();
5app.use(express.json());
6
7const 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';
10
11const 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});
19
20// Get all campaigns for the account
21app.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: 100
27 };
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});
38
39// Get analytics for all campaigns in the account
40app.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);
44
45 const formatDate = (d) => ({
46 day: d.getDate(),
47 month: d.getMonth() + 1,
48 year: d.getFullYear()
49 });
50
51 const start = formatDate(startDate);
52 const end = formatDate(endDate);
53
54 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});
74
75// Pause or resume a campaign
76app.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 updates
83 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});
91
92app.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.

5

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.

check_token.py
1import os
2import requests
3from datetime import datetime
4
5def 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": token
16 }
17 response = requests.post(url, data=data)
18 response.raise_for_status()
19 token_info = response.json()
20
21 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()).days
26 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.")
31
32 return token_info
33
34if __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.

Replit Prompt

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.

Replit Prompt

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.

Replit Prompt

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.

typescript
1# Python: build the authorization URL for re-authentication
2from urllib.parse import urlencode
3
4params = {
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.

typescript
1# Python: verify campaigns have data by checking Campaign Manager status
2campaigns = 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.

typescript
1# Python: correct PATCH format for LinkedIn
2response = requests.patch(
3 f"{BASE_URL}/adCampaignsV2/{campaign_id}",
4 json={'patch': {'$set': {'status': 'PAUSED'}}},
5 headers=HEADERS
6)
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

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.

RapidDev

Talk to an Expert

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

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

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.