How to Set Up LinkedIn Ads API Reporting
Automate LinkedIn Ads data collection and performance reporting.
Jay Banlasan
The AI Systems Guy
LinkedIn ads API reporting automation pulls your B2B ad data into the same pipeline as your other platforms. I set this up for any client running LinkedIn alongside Meta or Google. The data lands in your unified database without anyone logging into Campaign Manager.
LinkedIn's API is more restrictive than Meta's, but the data quality for B2B targeting is worth the extra setup.
What You Need Before Starting
- A LinkedIn Marketing Developer app (create at linkedin.com/developers)
- OAuth2 access token with
r_ads_reportingscope - Python 3.8+ with
requestsinstalled - Your LinkedIn Ad Account ID
Step 1: Set Up Authentication
LinkedIn uses OAuth2 with refresh tokens. Get your initial access token through the developer portal:
import requests
import os
from dotenv import load_dotenv
load_dotenv()
def refresh_linkedin_token():
resp = requests.post("https://www.linkedin.com/oauth/v2/accessToken", data={
"grant_type": "refresh_token",
"refresh_token": os.getenv("LINKEDIN_REFRESH_TOKEN"),
"client_id": os.getenv("LINKEDIN_CLIENT_ID"),
"client_secret": os.getenv("LINKEDIN_CLIENT_SECRET"),
})
data = resp.json()
return data.get("access_token")
def get_headers():
token = os.getenv("LINKEDIN_ACCESS_TOKEN")
return {
"Authorization": f"Bearer {token}",
"X-Restli-Protocol-Version": "2.0.0",
}
Step 2: Pull Campaign Analytics
from datetime import datetime, timedelta
def fetch_linkedin_ads(account_id, date=None):
if not date:
date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
headers = get_headers()
start = f"{date.replace('-', ',')}".split(",")
year, month, day = int(start[0]), int(start[1]), int(start[2])
url = "https://api.linkedin.com/rest/adAnalytics"
params = {
"q": "analytics",
"pivot": "CREATIVE",
"dateRange.start.day": day,
"dateRange.start.month": month,
"dateRange.start.year": year,
"dateRange.end.day": day,
"dateRange.end.month": month,
"dateRange.end.year": year,
"timeGranularity": "DAILY",
"accounts": f"urn:li:sponsoredAccount:{account_id}",
"fields": "impressions,clicks,costInLocalCurrency,externalWebsiteConversions,leads",
}
resp = requests.get(url, headers=headers, params=params)
data = resp.json()
rows = []
for element in data.get("elements", []):
rows.append({
"date": date,
"creative_id": element.get("pivotValue", ""),
"impressions": element.get("impressions", 0),
"clicks": element.get("clicks", 0),
"spend": float(element.get("costInLocalCurrency", 0)),
"conversions": element.get("externalWebsiteConversions", 0),
"leads": element.get("leads", 0),
})
return rows
Step 3: Get Campaign Names
LinkedIn's analytics endpoint returns URNs, not names. Resolve them:
def get_creative_details(creative_urns):
headers = get_headers()
details = {}
for urn in creative_urns:
creative_id = urn.split(":")[-1]
resp = requests.get(f"https://api.linkedin.com/rest/adCreatives/{creative_id}",
headers=headers)
data = resp.json()
campaign_urn = data.get("campaign", "")
campaign_id = campaign_urn.split(":")[-1] if campaign_urn else ""
# Get campaign name
if campaign_id:
camp_resp = requests.get(f"https://api.linkedin.com/rest/adCampaigns/{campaign_id}",
headers=headers)
campaign_name = camp_resp.json().get("name", "")
else:
campaign_name = ""
details[urn] = {
"creative_id": creative_id,
"campaign_name": campaign_name,
"campaign_id": campaign_id,
}
return details
Step 4: Store in Your Unified Database
import sqlite3
def store_linkedin_data(db_path, rows, details):
conn = sqlite3.connect(db_path)
conn.execute("""CREATE TABLE IF NOT EXISTS linkedin_daily (
id INTEGER PRIMARY KEY, date TEXT, creative_id TEXT,
campaign_name TEXT, impressions INTEGER, clicks INTEGER,
spend REAL, conversions INTEGER, leads INTEGER,
cpc REAL, ctr REAL, fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP
)""")
for row in rows:
detail = details.get(row["creative_id"], {})
cpc = row["spend"] / row["clicks"] if row["clicks"] > 0 else 0
ctr = (row["clicks"] / row["impressions"] * 100) if row["impressions"] > 0 else 0
conn.execute("""INSERT INTO linkedin_daily
(date, creative_id, campaign_name, impressions, clicks, spend, conversions, leads, cpc, ctr)
VALUES (?,?,?,?,?,?,?,?,?,?)""",
(row["date"], row["creative_id"], detail.get("campaign_name", ""),
row["impressions"], row["clicks"], row["spend"],
row["conversions"], row["leads"], round(cpc, 2), round(ctr, 2)))
conn.commit()
conn.close()
Step 5: Run Daily
if __name__ == "__main__":
account_id = os.getenv("LINKEDIN_AD_ACCOUNT_ID")
rows = fetch_linkedin_ads(account_id)
if rows:
creative_urns = [r["creative_id"] for r in rows]
details = get_creative_details(creative_urns)
store_linkedin_data("ads_unified.db", rows, details)
print(f"Stored {len(rows)} LinkedIn ad rows")
0 7 * * * cd /path/to/project && python3 linkedin_pull.py >> /var/log/linkedin.log 2>&1
LinkedIn data now flows alongside your Meta and Google data. Same queries, same reports, same alerts.
What to Build Next
Add LinkedIn data to your cross-platform dashboard and comparison reports. Then build a LinkedIn-specific cost per lead analysis that factors in lead quality from your CRM.
Related Reading
- AI in Paid Advertising: The Complete Overview - full ad operations across platforms
- The Integration Hierarchy - prioritizing platform integrations
- Cross-Functional AI: When Marketing Talks to Operations - connecting data across functions
Want this system built for your business?
Get a free assessment. We will map every system your business needs and show you the ROI.
Get Your Free Assessment