# Export

> Export Spotify Ads API campaign data to CSV — full campaign hierarchies with ad sets, ads, targeting, budgets, and performance metrics for offline review, campaign analysis, or budget reconciliation.

- Skill: `spotify/export` (Agent Skill)
- Install (CLI): `npx skillmds@latest add spotify/export`
- Raw SKILL.md: https://api.skillmd.com/api/skills/spotify/export/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Marketing & Growth
- Author: spotify (https://skillmd.com/u/spotify)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/spotify/export

---


# 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:

```bash
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

```bash
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:

```bash
api GET "ad_accounts/{ad_account_id}/campaigns/$CAMPAIGN_ID"
```

### Fetch ad sets

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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_objective`
- `ad_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_delivery`
- `ad_set_geo_country`, `ad_set_geo_regions`, `ad_set_age_min`, `ad_set_age_max`, `ad_set_platforms`, `ad_set_placements`, `ad_set_genders`
- `ad_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_amount` by 1,000,000 to display in the billing currency (e.g., `50000000` → `50.00`).
- **Metric SPEND**: Values from `aggregate_reports` are 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 `min` and `max` into `ad_set_age_min` and `ad_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 (via `campaign_id`).
- Metrics join on `entity_id` from the report rows to the entity's `id`.
- 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:

```python
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_execute` is `true`, execute all API calls directly and write the file.
- If `auto_execute` is `false`, 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:bulk` for batch changes.

