Spotify Ads API — Campaign Data Export
Export campaign hierarchies to CSV for offline review, combining entity data with optional performance metrics.
Setup
Set the plugin root and define the request wrapper:
PLUGIN_ROOT="${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.}}"
api() { "$PLUGIN_ROOT/scripts/api-request.sh" export "$@"; }
Before the first Ads API v3 call, read and follow $PLUGIN_ROOT/skills/api-reference/references/live-openapi.md.
To retrieve settings values (TOKEN, AD_ACCOUNT_ID, AUTO_EXECUTE, BASE_URL, SDK_HEADER, SKILL_HEADER, PLUGIN_VERSION) for use outside API calls, run api --env. The output is eval-safe, so eval $(api --env) assigns them all.
Parsing Arguments
- No argument → Export all campaigns
<campaign_id>(UUID) → Export a specific campaign--metrics→ Include performance metrics (impressions, spend, reach, etc.)--date-range <start> <end>→ Metric date range (ISO 8601). Default: last 30 days.- If ambiguous, ask the user.
Step 1: Ask Export Preferences
Ask the user to confirm:
- Scope: All campaigns or a specific campaign?
- Include metrics? Entity data only, or include performance metrics?
- Output path: Default
./spotify-ads-export-YYYY-MM-DD.csv, or user-specified path.
Step 2: Fetch Entity Data
Fetch all entity data with full pagination. Unlike other skills that show the first page, export must retrieve every entity to produce a complete file.
Fetch campaigns
api GET "ad_accounts/{ad_account_id}/campaigns?limit=50&offset=0"
Check paging.total_results in the response. If total_results > 50, make additional requests incrementing offset by 50 until all campaigns are fetched. For a single-campaign export, use:
api GET "ad_accounts/{ad_account_id}/campaigns/$CAMPAIGN_ID"
Fetch ad sets
api GET "ad_accounts/{ad_account_id}/ad_sets?limit=50&offset=0"
For a single campaign: add &campaign_ids=$CAMPAIGN_ID. Paginate with offset until all ad sets are fetched.
Fetch ads
api GET "ad_accounts/{ad_account_id}/ads?limit=50&offset=0"
For a single campaign: add &campaign_ids=$CAMPAIGN_ID. Paginate with offset until all ads are fetched.
Step 3: Fetch Metrics (if requested)
When metrics are included, fetch aggregate reports at each entity level. For a single-campaign export, add entity_ids=$CAMPAIGN_ID&entity_ids_type=CAMPAIGN to every report request so the export does not rely on the first unfiltered page containing the requested campaign's metrics.
Campaign-level metrics
api GET "ad_accounts/{ad_account_id}/aggregate_reports?\
entity_type=CAMPAIGN&\
fields=IMPRESSIONS&fields=SPEND&fields=CLICKS&fields=REACH&fields=FREQUENCY&fields=CTR&fields=COMPLETES&\
granularity=LIFETIME&\
entity_status_type=CAMPAIGN&\
limit=50"
If a date range is specified, switch to granularity=DAY and add &report_start=<start>&report_end=<end>.
Use UTC midnight timestamps such as 2026-05-01T00:00:00Z. Do not send report_start or report_end with granularity=LIFETIME.
Paginate with continuation_token if present in the response.
Ad set-level metrics
api GET "ad_accounts/{ad_account_id}/aggregate_reports?\
entity_type=AD_SET&\
fields=IMPRESSIONS&fields=SPEND&fields=CLICKS&fields=REACH&fields=FREQUENCY&fields=COMPLETES&fields=COMPLETION_RATE&\
granularity=LIFETIME&\
entity_status_type=AD_SET&\
include_parent_entity=true&\
limit=50"
For a single-campaign export, add &entity_ids=$CAMPAIGN_ID&entity_ids_type=CAMPAIGN. Paginate with continuation_token if present in the response.
Ad-level metrics
api GET "ad_accounts/{ad_account_id}/aggregate_reports?\
entity_type=AD&\
fields=IMPRESSIONS&fields=SPEND&fields=CLICKS&fields=REACH&\
granularity=LIFETIME&\
entity_status_type=AD&\
include_parent_entity=true&\
limit=50"
For a single-campaign export, add &entity_ids=$CAMPAIGN_ID&entity_ids_type=CAMPAIGN. Paginate with continuation_token if present.
Step 4: Build and Write CSV
CSV columns
The CSV is denormalized — one row per ad, with campaign and ad set data repeated on each row. Ad sets with no ads get a row with blank ad columns.
Entity columns:
campaign_id,campaign_name,campaign_status,campaign_objectivead_set_id,ad_set_name,ad_set_status,ad_set_format,ad_set_budget_type,ad_set_budget_amount,ad_set_bid_strategy,ad_set_bid_amount,ad_set_start_time,ad_set_end_time,ad_set_deliveryad_set_geo_country,ad_set_geo_regions,ad_set_age_min,ad_set_age_max,ad_set_platforms,ad_set_placements,ad_set_gendersad_id,ad_name,ad_status,ad_delivery,ad_tagline,ad_advertiser_name,ad_cta_key,ad_cta_url
Metric columns (when --metrics is used):
impressions,spend,clicks,reach,frequency,ctr,completes,completion_rate
Data transformations
- Budget/bid amounts: Divide
micro_amountby 1,000,000 to display in the billing currency (e.g.,50000000→50.00). - Metric SPEND: Values from
aggregate_reportsare already in the billing currency — display directly. - Geo targeting: Flatten to
ad_set_geo_country= country code string,ad_set_geo_regions= comma-separated region/DMA/city names if available (IDs if names are not in the response). Note: DMA-level targeting (dma_ids) is no longer available for new ad sets, but older campaigns may still contain DMA data in their geo targets. - Age ranges: Extract first range's
minandmaxintoad_set_age_minandad_set_age_max. - Arrays (platforms, placements, genders): Join with commas (e.g.,
"ANDROID,DESKTOP,IOS"). - CSV quoting: Wrap values containing commas, quotes, or newlines in double quotes. Escape internal double quotes by doubling them (
"").
Join logic
Match entities by ID:
- Each ad belongs to an ad set (via
ad_set_id) which belongs to a campaign (viacampaign_id). - Metrics join on
entity_idfrom the report rows to the entity'sid. - If an entity has no metrics (zero impressions, new campaign), include the row with blank metric columns.
Write the file
Write the CSV header and rows with a structured CSV writer. Prefer Python's standard csv module, or use jq @csv if all rows are already available as JSON. Do not build CSV rows with echo or string concatenation; that will corrupt values containing commas, quotes, or newlines.
Example Python shape:
import csv
columns = [
"campaign_id", "campaign_name", "campaign_status", "campaign_objective",
"ad_set_id", "ad_set_name", "ad_set_status", "ad_set_format",
"ad_set_budget_type", "ad_set_budget_amount", "ad_set_start_time",
"ad_set_end_time", "ad_id", "ad_name", "ad_status", "ad_delivery",
"impressions", "spend", "clicks", "reach",
]
with open(output_path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=columns, extrasaction="ignore")
writer.writeheader()
writer.writerows(rows)
Build rows from parsed API JSON before writing. Let the CSV writer handle quoting and escaping.
Step 5: Display Summary
After writing the file, display:
Export complete: ./spotify-ads-export-2026-05-14.csv
Campaigns: 3
Ad Sets: 7
Ads: 12
Metrics included: Yes (lifetime, last 30 days)
File size: 4.2 KB
Execution Behavior
- If
auto_executeistrue, execute all API calls directly and write the file. - If
auto_executeisfalse, present the curl commands for the first fetch and ask for confirmation before executing. After confirmation, execute all remaining fetches without additional prompts. - Always check the
HTTP_STATUS:line from curl output to determine success or failure before interpreting the response body. - On error from any fetch, show the error and continue with available data. Note which entity types are missing from the export.
- For large accounts (50+ campaigns), note that pagination will require multiple API calls and may take 30-60 seconds.
Cross-references
- For server-generated async CSV reports with different column structure, use
/spotify-ads-api:report async-create. - For a quick visual overview instead of a file export, use
/spotify-ads-api:dashboard. - After reviewing exported data, use
/spotify-ads-api:bulkfor batch changes.