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:
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
api GET "ad_accounts/{ad_account_id}/campaigns?statuses=ACTIVE&limit=50&sort_direction=DESC"
2. Active ad sets
api GET "ad_accounts/{ad_account_id}/ad_sets?statuses=ACTIVE&limit=50&sort_direction=DESC"
3. Today's campaign metrics
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)
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%)
- Display:
-
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) * 100spend_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_elapseddays_remaining_at_pace = (budget - spend) / daily_burnprojected_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
api GET "ad_accounts/{ad_account_id}/campaigns/$CAMPAIGN_ID"
2. All ad sets under the campaign
api GET "ad_accounts/{ad_account_id}/ad_sets?campaign_ids=$CAMPAIGN_ID&limit=50"
3. All ads under the campaign
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:
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
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
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
PENDINGwherecreated_atis more than 24 hours ago — flag as unusually slow approval. - Delivery OFF: Ads with
delivery: OFFunder 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_executeistrue, execute all API calls directly and display the health check. - If
auto_executeisfalse, 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_amountfrom 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%).