Systems Library / Marketing Automation / How to Set Up LinkedIn Ads API Reporting
Marketing Automation paid advertising

How to Set Up LinkedIn Ads API Reporting

Automate LinkedIn Ads data collection and performance reporting.

Jay Banlasan

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

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

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

Related Systems