Marketing Reports Skill
Query advertising campaigns and performance metrics from LinkedIn Ads, Google Ads, Meta Ads (Facebook), and Microsoft Ads (Bing) using their official APIs.
Why This Skill Exists
Marketing teams need unified access to campaign data across multiple ad platforms. This skill provides:
- Consistent data models across platforms
- Python helper scripts for common queries
- Direct API access patterns for advanced use cases
Quick Start
# Run the unified query script (auto-loads credentials, see below)
python scripts/query_campaigns.py --platform all --days 7
# Get LinkedIn campaigns only
python scripts/query_campaigns.py --platform linkedin --status ACTIVE
# Export to JSON
python scripts/query_campaigns.py --platform google --output campaigns.json
# Explicit credential file
python scripts/query_campaigns.py --env-file /path/to/.env --platform linkedin
Credential Loading (Auto-Detection)
The script automatically searches for .env files in this priority order:
- Source skill directory (
agent-skills/marketing-reports/.env)
- Installed skill directories (
~/.claude/skills/marketing-reports/.env or ~/.opencode/skills/marketing-reports/.env)
- Current working directory (
.env)
- Explicit
--env-file argument (highest priority, overrides all others)
Recommended: After installing the skill with openskills sync, put credentials in the installed location (e.g., ~/.claude/skills/marketing-reports/.env). This keeps credentials separate from source code.
Prerequisites & Authentication
Environment Variables
Create a .env file with credentials for each platform you use:
# LinkedIn Marketing API
LINKEDIN_ACCESS_TOKEN="your-oauth2-bearer-token"
LINKEDIN_ACCOUNT_ID="urn:li:sponsoredAccount:508860617"
# Google Ads API
GOOGLE_ADS_DEVELOPER_TOKEN="your-developer-token"
GOOGLE_ADS_CLIENT_ID="your-oauth-client-id"
GOOGLE_ADS_CLIENT_SECRET="your-oauth-client-secret"
GOOGLE_ADS_REFRESH_TOKEN="your-refresh-token"
# Meta (Facebook) Ads API
META_APP_ID="your-app-id"
META_APP_SECRET="your-app-secret"
META_ACCESS_TOKEN="your-user-access-token"
# Microsoft (Bing) Ads API
MICROSOFT_ADS_CLIENT_ID="your-client-id"
MICROSOFT_ADS_CLIENT_SECRET="your-client-secret"
MICROSOFT_ADS_DEVELOPER_TOKEN="your-developer-token"
MICROSOFT_ADS_REFRESH_TOKEN="your-refresh-token"
MICROSOFT_ADS_CUSTOMER_ID="your-customer-id"
MICROSOFT_ADS_ACCOUNT_ID="your-account-id"
Required Python Packages
pip install httpx google-ads facebook_business bingads python-dotenv
Part 1: LinkedIn Marketing API
API Overview
| Field |
Value |
| Base URL |
https://api.linkedin.com/rest/ |
| Auth |
OAuth2 Bearer token |
| Version Header |
LinkedIn-Version: 202511 |
| Rate Limit |
100 requests/day per member |
List Campaigns
# Get all campaigns for an ad account
ACCOUNT_ID="508860617" # Numeric ID, not URN
curl -s -X GET "https://api.linkedin.com/rest/adAccounts/${ACCOUNT_ID}/adCampaigns?q=search&count=100" \
-H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \
-H "LinkedIn-Version: 202511" \
-H "X-Restli-Protocol-Version: 2.0.0" | jq '.'
Response:
{
"elements": [
{
"id": 123456789,
"name": "Brand Awareness Q1",
"status": "ACTIVE",
"type": "SPONSORED_UPDATES",
"dailyBudget": {"amount": "100.00", "currencyCode": "USD"},
"runSchedule": {"start": 1704067200000, "end": null}
}
],
"metadata": {"nextPageToken": "..."}
}
Get Campaign Analytics
# Get performance metrics for the last 7 days
ACCOUNT_URN="urn:li:sponsoredAccount:508860617"
START_DATE="2026-01-24"
END_DATE="2026-01-31"
curl -s -X GET "https://api.linkedin.com/rest/adAnalytics?q=analytics&pivot=CAMPAIGN\
&dateRange=(start:(year:2026,month:1,day:24),end:(year:2026,month:1,day:31))\
&timeGranularity=ALL\
&accounts=List(${ACCOUNT_URN})\
&fields=clicks,impressions,costInLocalCurrency,oneClickLeads,pivotValues,dateRange" \
-H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \
-H "LinkedIn-Version: 202511" \
-H "X-Restli-Protocol-Version: 2.0.0" | jq '.'
Response:
{
"elements": [
{
"impressions": 15420,
"clicks": 342,
"costInLocalCurrency": 856.50,
"oneClickLeads": 12,
"pivotValues": ["urn:li:sponsoredCampaign:123456789"]
}
]
}
Update Campaign Status
CAMPAIGN_URN="urn:li:sponsoredCampaign:123456789"
ENCODED_URN=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$CAMPAIGN_URN', safe=''))")
# Pause a campaign
curl -s -X PATCH "https://api.linkedin.com/rest/adCampaigns/${ENCODED_URN}" \
-H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \
-H "LinkedIn-Version: 202511" \
-H "Content-Type: application/json" \
-d '{"patch": {"$set": {"status": "PAUSED"}}}' | jq '.'
LinkedIn Status Values
| Status |
Description |
ACTIVE |
Campaign is running |
PAUSED |
Manually paused |
DRAFT |
Not yet launched |
CANCELED |
Permanently stopped |
COMPLETED |
End date reached |
ARCHIVED |
Archived for reporting |
LinkedIn Campaign Types
| Type |
Description |
SPONSORED_UPDATES |
Sponsored content in feed |
TEXT_AD |
Text ads in sidebar |
SPONSORED_INMAILS |
Message ads |
DYNAMIC |
Dynamic ads |
Part 2: Google Ads API
API Overview
| Field |
Value |
| SDK |
google-ads Python library |
| Auth |
OAuth2 + Developer token |
| Version |
v17 (latest) |
| Rate Limit |
15,000 requests/day |
Initialize Client
from google.ads.googleads.client import GoogleAdsClient
config = {
"developer_token": os.environ["GOOGLE_ADS_DEVELOPER_TOKEN"],
"client_id": os.environ["GOOGLE_ADS_CLIENT_ID"],
"client_secret": os.environ["GOOGLE_ADS_CLIENT_SECRET"],
"refresh_token": os.environ["GOOGLE_ADS_REFRESH_TOKEN"],
"use_proto_plus": True,
}
client = GoogleAdsClient.load_from_dict(config)
List Accessible Customers
customer_service = client.get_service("CustomerService")
response = customer_service.list_accessible_customers()
customer_ids = [r.split("/")[-1] for r in response.resource_names]
print(f"Accessible accounts: {customer_ids}")
Get Campaigns with Metrics
ga_service = client.get_service("GoogleAdsService")
customer_id = "1234567890" # Your customer ID
query = """
SELECT
campaign.id,
campaign.name,
campaign.status,
campaign.advertising_channel_type,
campaign_budget.amount_micros,
metrics.impressions,
metrics.clicks,
metrics.cost_micros,
metrics.conversions
FROM campaign
WHERE campaign.status != 'REMOVED'
AND segments.date DURING LAST_7_DAYS
ORDER BY metrics.impressions DESC
"""
response = ga_service.search(customer_id=customer_id, query=query)
for row in response:
campaign = row.campaign
metrics = row.metrics
budget = row.campaign_budget.amount_micros / 1_000_000
spend = metrics.cost_micros / 1_000_000
print(f"{campaign.name}: {metrics.impressions} impressions, ${spend:.2f} spend")
Google Ads Status Values
| Status |
Description |
ENABLED |
Campaign is active |
PAUSED |
Manually paused |
REMOVED |
Deleted |
Google Ads Channel Types
| Channel |
Description |
SEARCH |
Search network ads |
DISPLAY |
Display network ads |
SHOPPING |
Shopping campaigns |
VIDEO |
YouTube ads |
PERFORMANCE_MAX |
Automated cross-channel |
Filter by Date Range
# Custom date range
from_date = "2026-01-01"
to_date = "2026-01-31"
query = f"""
SELECT campaign.id, campaign.name, metrics.impressions, metrics.clicks
FROM campaign
WHERE segments.date BETWEEN '{from_date}' AND '{to_date}'
"""
Part 3: Meta (Facebook) Ads API
API Overview
| Field |
Value |
| SDK |
facebook_business Python library |
| Auth |
OAuth2 App token |
| Version |
v24.0 |
| Rate Limit |
Varies by endpoint (see docs) |
Initialize API
from facebook_business.api import FacebookAdsApi
from facebook_business.adobjects.user import User
from facebook_business.adobjects.adaccount import AdAccount
from facebook_business.adobjects.campaign import Campaign as FBCampaign
FacebookAdsApi.init(
app_id=os.environ["META_APP_ID"],
app_secret=os.environ["META_APP_SECRET"],
access_token=os.environ["META_ACCESS_TOKEN"]
)
List Ad Accounts
me = User(fbid='me')
accounts = list(me.get_ad_accounts(fields=['account_id', 'name', 'account_status']))
for account in accounts:
print(f"{account['name']}: act_{account['account_id']}")
Get Campaigns
account_id = "act_1234567890" # Must include 'act_' prefix
account = AdAccount(account_id)
campaigns = account.get_campaigns(
fields=[
FBCampaign.Field.id,
FBCampaign.Field.name,
FBCampaign.Field.status,
FBCampaign.Field.effective_status,
FBCampaign.Field.objective,
FBCampaign.Field.daily_budget,
FBCampaign.Field.lifetime_budget,
]
)
for campaign in campaigns:
daily_budget = float(campaign.get('daily_budget', 0) or 0) / 100 # Cents to dollars
print(f"{campaign['name']}: {campaign['effective_status']}, ${daily_budget}/day")
Get Campaign Insights (Analytics)
from facebook_business.adobjects.adsinsights import AdsInsights
campaign = FBCampaign("123456789") # Campaign ID
insights = campaign.get_insights(
fields=[
AdsInsights.Field.impressions,
AdsInsights.Field.clicks,
AdsInsights.Field.spend,
AdsInsights.Field.ctr,
AdsInsights.Field.cpc,
AdsInsights.Field.actions,
],
params={
'date_preset': 'last_7d', # Or 'last_30d', 'today', 'lifetime'
'level': 'campaign',
}
)
for insight in insights:
print(f"Impressions: {insight['impressions']}")
print(f"Clicks: {insight['clicks']}")
print(f"Spend: ${insight['spend']}")
Meta Status Values
| Status |
Description |
ACTIVE |
Campaign is running |
PAUSED |
Manually paused |
DELETED |
Removed |
ARCHIVED |
Archived |
Meta Campaign Objectives
| Objective |
Description |
OUTCOME_AWARENESS |
Brand awareness |
OUTCOME_ENGAGEMENT |
Engagement |
OUTCOME_LEADS |
Lead generation |
OUTCOME_SALES |
Conversions |
OUTCOME_TRAFFIC |
Website traffic |
Date Presets for Insights
| Preset |
Description |
today |
Today only |
yesterday |
Yesterday only |
last_7d |
Last 7 days |
last_14d |
Last 14 days |
last_30d |
Last 30 days |
last_90d |
Last 90 days |
lifetime |
All time |
Part 4: Microsoft (Bing) Ads API
API Overview
| Field |
Value |
| SDK |
bingads Python library |
| Auth |
OAuth2 + Developer token |
| Version |
v13 |
| Rate Limit |
Varies by operation |
Initialize Client
from bingads import AuthorizationData, OAuthWebAuthCodeGrant, ServiceClient
authentication = OAuthWebAuthCodeGrant(
client_id=os.environ["MICROSOFT_ADS_CLIENT_ID"],
client_secret=os.environ["MICROSOFT_ADS_CLIENT_SECRET"],
redirection_uri="https://localhost:8080/callback",
)
authentication.request_oauth_tokens_by_refresh_token(
os.environ["MICROSOFT_ADS_REFRESH_TOKEN"]
)
authorization_data = AuthorizationData(
account_id=os.environ.get("MICROSOFT_ADS_ACCOUNT_ID"),
customer_id=os.environ.get("MICROSOFT_ADS_CUSTOMER_ID"),
developer_token=os.environ["MICROSOFT_ADS_DEVELOPER_TOKEN"],
authentication=authentication,
)
List Ad Accounts
customer_service = ServiceClient(
service='CustomerManagementService',
version=13,
authorization_data=authorization_data,
environment='production',
)
accounts_response = customer_service.SearchAccounts(
PageInfo={'Index': 0, 'Size': 100},
Predicates=None,
)
for account in accounts_response.AdvertiserAccount:
print(f"{account.Name}: ID={account.Id}, Status={account.AccountLifeCycleStatus}")
Get Campaigns
campaign_service = ServiceClient(
service='CampaignManagementService',
version=13,
authorization_data=authorization_data,
environment='production',
)
account_id = 123456789
response = campaign_service.GetCampaignsByAccountId(
AccountId=account_id,
CampaignType='Search Shopping Audience',
)
for campaign in response.Campaign:
print(f"{campaign.Name}: {campaign.Status}, ${campaign.DailyBudget}/day")
Microsoft Ads Status Values
| Status |
Description |
Active |
Campaign is running |
Paused |
Manually paused |
Deleted |
Removed |
Microsoft Ads Campaign Types
| Type |
Description |
Search |
Search network |
Shopping |
Shopping campaigns |
Audience |
Audience campaigns |
Part 5: Unified Data Models
All platforms are normalized to these data structures:
Campaign
@dataclass
class Campaign:
id: str # Platform-specific ID
name: str # Campaign name
status: str # ACTIVE, PAUSED, etc. (normalized)
campaign_type: str # Platform-specific type
daily_budget: float # Daily spend limit
total_budget: Optional[float] # Total/lifetime budget
currency: str # USD, EUR, etc.
start_date: datetime # Campaign start
end_date: Optional[datetime] # Campaign end (if set)
account_id: str # Parent ad account
platform: str # linkedin, google, meta, microsoft
# Analytics (populated from metrics)
spend: float = 0.0
impressions: int = 0
clicks: int = 0
conversions: int = 0
@property
def ctr(self) -> float:
"""Click-through rate percentage."""
return (self.clicks / self.impressions * 100) if self.impressions > 0 else 0.0
@property
def cpc(self) -> float:
"""Cost per click."""
return (self.spend / self.clicks) if self.clicks > 0 else 0.0
CampaignAnalytics
@dataclass
class CampaignAnalytics:
campaign_id: str
date_start: datetime
date_end: datetime
impressions: int = 0
clicks: int = 0
cost: float = 0.0
conversions: int = 0
conversion_value: float = 0.0
@property
def ctr(self) -> float:
return (self.clicks / self.impressions * 100) if self.impressions > 0 else 0.0
@property
def cpc(self) -> float:
return (self.cost / self.clicks) if self.clicks > 0 else 0.0
@property
def cpm(self) -> float:
return (self.cost / self.impressions * 1000) if self.impressions > 0 else 0.0
@property
def conversion_rate(self) -> float:
return (self.conversions / self.clicks * 100) if self.clicks > 0 else 0.0
@property
def cost_per_conversion(self) -> float:
return (self.cost / self.conversions) if self.conversions > 0 else 0.0
Part 6: Common Queries
Get All Active Campaigns Across Platforms
from scripts.query_campaigns import get_all_campaigns
# Returns unified list of Campaign objects
campaigns = get_all_campaigns(
platforms=["linkedin", "google", "meta", "microsoft"],
status_filter="ACTIVE",
days=7
)
for c in campaigns:
print(f"[{c.platform}] {c.name}: ${c.spend:.2f} spend, {c.clicks} clicks")
Compare Platform Performance
from collections import defaultdict
by_platform = defaultdict(lambda: {"spend": 0, "clicks": 0, "impressions": 0})
for c in campaigns:
by_platform[c.platform]["spend"] += c.spend
by_platform[c.platform]["clicks"] += c.clicks
by_platform[c.platform]["impressions"] += c.impressions
for platform, metrics in by_platform.items():
ctr = (metrics["clicks"] / metrics["impressions"] * 100) if metrics["impressions"] > 0 else 0
print(f"{platform}: ${metrics['spend']:.2f} spend, {ctr:.2f}% CTR")
Export to CSV
import csv
with open("campaigns_report.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["Platform", "Campaign", "Status", "Spend", "Impressions", "Clicks", "CTR"])
for c in campaigns:
writer.writerow([c.platform, c.name, c.status, c.spend, c.impressions, c.clicks, f"{c.ctr:.2f}%"])
Part 7: Error Handling
Common Errors and Solutions
| Error |
Platform |
Solution |
| 401 Unauthorized |
All |
Refresh OAuth token or check credentials |
| 429 Rate Limited |
All |
Wait for retry-after header duration |
| 403 Forbidden |
LinkedIn |
Check account permissions |
| Token expired |
All |
Re-authenticate via OAuth flow |
| Account disabled |
Google |
Contact Google support |
Retry Logic
import time
from functools import wraps
def retry_on_rate_limit(max_retries=3, base_delay=60):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except RateLimitError as e:
if attempt < max_retries - 1:
delay = getattr(e, 'retry_after', base_delay)
print(f"Rate limited. Waiting {delay}s...")
time.sleep(delay)
else:
raise
return None
return wrapper
return decorator
Quick Reference
API Endpoints Summary
| Platform |
Campaigns Endpoint |
Analytics Endpoint |
| LinkedIn |
/rest/adAccounts/{id}/adCampaigns |
/rest/adAnalytics?pivot=CAMPAIGN |
| Google |
GAQL: SELECT FROM campaign |
GAQL: SELECT metrics.* FROM campaign |
| Meta |
AdAccount.get_campaigns() |
Campaign.get_insights() |
| Microsoft |
CampaignManagementService.GetCampaignsByAccountId |
ReportingService |
Status Normalization
| Normalized |
LinkedIn |
Google |
Meta |
Microsoft |
| ACTIVE |
ACTIVE |
ENABLED |
ACTIVE |
Active |
| PAUSED |
PAUSED |
PAUSED |
PAUSED |
Paused |
| DELETED |
CANCELED |
REMOVED |
DELETED |
Deleted |
Metrics Field Mapping
| Metric |
LinkedIn |
Google |
Meta |
Microsoft |
| Impressions |
impressions |
metrics.impressions |
impressions |
Impressions |
| Clicks |
clicks |
metrics.clicks |
clicks |
Clicks |
| Spend |
costInLocalCurrency |
metrics.cost_micros / 1M |
spend |
Spend |
| Conversions |
externalWebsiteConversions |
metrics.conversions |
actions[].value |
Conversions |
Related Resources