# Monitor

> Check Spotify Ads API campaign health — pacing, delivery issues, budget burn rate, stalled campaigns, and underpacing alerts. Use for one-shot health checks or recurring monitoring when the host supports scheduled automations.

- Skill: `spotify/monitor` (Agent Skill)
- Install (CLI): `npx skillmds@latest add spotify/monitor`
- Raw SKILL.md: https://api.skillmd.com/api/skills/spotify/monitor/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/monitor

---


# Spotify Ads API — Campaign Health Monitor

Diagnose delivery problems across active campaigns. Goes beyond the dashboard by running diagnostic checks and recommending specific actions.

## 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" monitor "$@"; }
```

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 or `--all` → Health check all active campaigns
- `<campaign_id>` (UUID) → Deep health check on a specific campaign (includes ad set and ad level diagnostics)
- If ambiguous, ask the user.

---

## Account-Wide Health Check (default)

### API Calls

Execute these calls to gather the data needed for diagnostics:

#### 1. Active campaigns

```bash
api GET "ad_accounts/{ad_account_id}/campaigns?statuses=ACTIVE&limit=50&sort_direction=DESC"
```

#### 2. Active ad sets

```bash
api GET "ad_accounts/{ad_account_id}/ad_sets?statuses=ACTIVE&limit=50&sort_direction=DESC"
```

#### 3. Today's campaign metrics

```bash
api GET "ad_accounts/{ad_account_id}/aggregate_reports?\
entity_type=CAMPAIGN&\
fields=IMPRESSIONS&fields=SPEND&fields=REACH&fields=CLICKS&fields=CTR&fields=FREQUENCY&\
granularity=DAY&\
report_start=$(date -u +%Y-%m-%dT00:00:00Z)&\
report_end=$(date -u +%Y-%m-%dT00:00:00Z)&\
entity_status_type=CAMPAIGN&\
statuses=ACTIVE&\
limit=50"
```

#### 4. Lifetime campaign metrics (for pacing)

```bash
api GET "ad_accounts/{ad_account_id}/aggregate_reports?\
entity_type=CAMPAIGN&\
fields=IMPRESSIONS&fields=SPEND&fields=REACH&fields=CLICKS&\
granularity=LIFETIME&\
entity_status_type=CAMPAIGN&\
statuses=ACTIVE&\
limit=50"
```

### Health Checks

Run these diagnostics against the fetched data:

#### 1. Pacing Check

For each active campaign, compare spend progress to flight progress:

- **DAILY budgets**: Sum the daily budgets of all active ad sets under the campaign. Compare today's spend to the total daily budget.
  - Display: `$142.50 / $200.00 daily (71%)`

- **LIFETIME budgets**: Compare total lifetime spend to total lifetime budget, and compare against elapsed flight percentage.
  - `elapsed_pct = (today - start_date) / (end_date - start_date) * 100`
  - `spend_pct = total_spend / total_budget * 100`
  - Display: `$2,100 / $5,000 lifetime (42% spent, 35% of flight elapsed)`

**Alert levels:**
- **OK**: Spend % and elapsed % within 20 points of each other
- **WARNING**: 20-40 point gap between spend % and elapsed %
- **CRITICAL**: >40 point gap, OR zero spend with >24 hours elapsed since campaign start

#### 2. Stalled Delivery Check

- If an ACTIVE campaign has zero impressions today AND its start_time is more than 4 hours ago, flag as **STALLED**.
- Exclude campaigns that started within the last 4 hours (they may still be ramping up).

#### 3. Budget Exhaustion Check

For LIFETIME budgets, project when the budget will run out at the current daily burn rate:
- `daily_burn = lifetime_spend / days_elapsed`
- `days_remaining_at_pace = (budget - spend) / daily_burn`
- `projected_end = today + days_remaining_at_pace`

Flag if:
- Budget will exhaust **before** the scheduled `end_time` → "May exhaust budget early"
- Budget will be **>20% unspent** at `end_time` → "Likely to underspend"

### Display Format

```
=== Campaign Health Check ===
Checked: 3 active campaigns, 7 active ad sets
Time: 2026-05-14 14:30 UTC

| Campaign | Status | Spend Today | Pacing | Issues |
|----------|--------|-------------|--------|--------|
| Summer Promo | OK | $142.50 / $200 daily (71%) | On track | — |
| Q2 Brand | WARNING | $28.00 / $100 daily (28%) | Underpacing | 1 stalled ad set |
| Podcast Launch | CRITICAL | $0.00 / $75 daily (0%) | Stalled | No delivery today |

Issues Found (2):
  1. [WARNING] Q2 Brand: Spending at 28% of daily budget with 62% of day elapsed
     → Consider increasing bid or broadening targeting. Run: /spotify-ads-api:dashboard <campaign_id>
  2. [CRITICAL] Podcast Launch: Zero impressions today (campaign started 2026-05-12)
     → Campaign may have delivery issues. Check ad approval status with: /spotify-ads-api:monitor <campaign_id>
```

If no issues are found:

```
All 3 active campaigns are healthy. No issues detected.
```

---

## Deep Campaign Health Check (`<campaign_id>`)

### API Calls

#### 1. Campaign details

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

#### 2. All ad sets under the campaign

```bash
api GET "ad_accounts/{ad_account_id}/ad_sets?campaign_ids=$CAMPAIGN_ID&limit=50"
```

#### 3. All ads under the campaign

```bash
api GET "ad_accounts/{ad_account_id}/ads?campaign_ids=$CAMPAIGN_ID&limit=50"
```

Extract active and paused ad set IDs from Step 2 and build repeated query parameters before fetching ad set metrics:

```bash
AD_SET_IDS_QUERY="entity_ids=<AD_SET_ID_1>&entity_ids=<AD_SET_ID_2>"
```

Use the ad set IDs directly because non-`LIFETIME` aggregate reports require `entity_ids_type` to match `entity_type`. If the campaign has more than 50 ad sets, chunk report requests in groups of 50.

#### 4. Today's ad set metrics

```bash
api GET "ad_accounts/{ad_account_id}/aggregate_reports?\
entity_type=AD_SET&\
fields=IMPRESSIONS&fields=SPEND&fields=CLICKS&fields=REACH&fields=CTR&fields=FREQUENCY&fields=COMPLETES&\
granularity=DAY&\
report_start=$(date -u +%Y-%m-%dT00:00:00Z)&\
report_end=$(date -u +%Y-%m-%dT00:00:00Z)&\
${AD_SET_IDS_QUERY}&\
entity_ids_type=AD_SET&\
entity_status_type=AD_SET&\
limit=50"
```

#### 5. Lifetime ad set 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&\
granularity=LIFETIME&\
${AD_SET_IDS_QUERY}&\
entity_ids_type=AD_SET&\
entity_status_type=AD_SET&\
include_parent_entity=true&\
limit=50"
```

### Additional Health Checks (Deep Mode)

In addition to the pacing, stalled, and exhaustion checks from the account-wide mode, the deep check adds:

#### 4. Ad Health Check

- **Rejected ads**: Ads with status `REJECTED` — flag with recommendation to review and re-create.
- **Pending ads**: Ads with status `PENDING` where `created_at` is more than 24 hours ago — flag as unusually slow approval.
- **Delivery OFF**: Ads with `delivery: OFF` under an ACTIVE ad set — flag as potentially unintentional.

#### 5. Audience Fatigue Check

- **High frequency**: If lifetime frequency > 3.0 on any ad set, warn about potential audience fatigue. Suggest expanding targeting or adding new creative.
- **Low CTR**: If CTR < 0.1% with > 10,000 impressions on any ad set, suggest reviewing creative or targeting.

### Display Format

```
=== Summer Promo — Deep Health Check ===
Campaign: ACTIVE | Objective: REACH | Created: 2026-04-01

Ad Set Health:
| Ad Set | Format | Budget | Spend Today | Pacing | Freq | Issues |
|--------|--------|--------|-------------|--------|------|--------|
| US 18-34 Audio | AUDIO | $75/day | $68.50 (91%) | OK | 1.8 | — |
| US 25-54 Video | VIDEO | $50/day | $12.00 (24%) | WARNING | 0.4 | Underpacing |

Ad Health:
| Ad | Ad Set | Status | Delivery | Issues |
|----|--------|--------|----------|--------|
| 30s Spot A | US 18-34 Audio | APPROVED | ON | — |
| 30s Spot B | US 18-34 Audio | REJECTED | — | Rejected |
| 15s Video | US 25-54 Video | PENDING | ON | Pending >24h |

Recommendations:
  1. "US 25-54 Video" is underpacing at 24% spend. Consider:
     - Broadening targeting (current: US, ages 25-54, VIDEO)
     - Increasing bid cap
     - Run: /spotify-ads-api:ads ad-sets get <ad_set_id>
  2. "30s Spot B" was rejected. Review and re-create via /spotify-ads-api:ads ads create
  3. "15s Video" has been pending approval for >24h — check if creative meets format requirements.
```

---

## Recurring Monitoring

After displaying results, suggest recurring checks only when the host environment supports scheduled tasks or automations:

```
To monitor automatically, schedule a recurring run of:
  /spotify-ads-api:monitor --all
```

---

## Execution Behavior

- If `auto_execute` is `true`, execute all API calls directly and display the health check.
- If `auto_execute` is `false`, present the curl commands and ask for confirmation before executing.
- Always check the `HTTP_STATUS:` line from curl output to determine success or failure before interpreting the response body.
- On error, show the error message from the response body and continue with available data.

## Formatting Rules

- **Spend from `aggregate_reports`**: Already in the ad account's billing currency — display directly as `$X.XX`.
- **Budget `micro_amount` from entity details**: In micro-units — divide by 1,000,000 to get the billing currency amount.
- **Impressions/Reach/Clicks**: Format with thousands separators (e.g., `156,234`).
- **CTR**: Display as percentage with 2 decimal places (e.g., `0.79%`).
- **Frequency**: Display with 1 decimal place.
- **Percentages**: Round to nearest integer for pacing (e.g., `71%`).

