with one click
build-campaign
// Create a full campaign (campaign + ad sets + ads) from a plain-text description. Parses natural language into structured API calls.
// Create a full campaign (campaign + ad sets + ads) from a plain-text description. Parses natural language into structured API calls.
Manage Spotify Ads API ad sets and ads — list, create, get, or update.
This skill should be used when the user asks to "call the Spotify Ads API", "create a Spotify ad campaign", "manage Spotify ads", "pull Spotify ad reports", "set up ad sets or ads", "upload ad assets", "target audiences on Spotify", "check campaign status", "get ad account info", "look up API schema or fields", "check what targeting options exist", or asks about Spotify advertising endpoints, request/response formats, enum values, or authentication.
Pull Spotify Ads API reporting data — aggregate metrics, audience insights, or async CSV reports.
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.
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.
Generate Spotify Ads campaign strategy from a landing page, product or business page, brand brief, location page, uploaded creative assets, existing Spotify Ads assets, or a natural-language business goal. Use when the user asks for the best campaign structure, targeting plan, audience plan, budget split, creative rotation, API-ready campaign plan, or pre-build recommendations before creating Spotify campaigns.
| name | build-campaign |
| description | Create a full campaign (campaign + ad sets + ads) from a plain-text description. Parses natural language into structured API calls. |
| argument-hint | <plain-text campaign description> |
| allowed-tools | ["Read","Bash","AskUserQuestion"] |
Given a plain-text description of an advertising campaign, parse it into structured API calls and create the full campaign hierarchy: Campaign → Ad Sets → Ads.
access_token, ad_account_id, and auto_execute from the active platform settings file:
.codex/spotify-ads-api.local.md, then fall back to .claude/spotify-ads-api.local.md..claude/spotify-ads-api.local.md, then fall back to .codex/spotify-ads-api.local.md.https://api-partner.spotify.com/ads/v3/spotify-ads-api:configure first.version: .codex-plugin/plugin.json on Codex or .claude-plugin/plugin.json on Claude.SDK_PRODUCT to codex-plugin on Codex or claude-code-plugin on Claude. Set SDK_HEADER="X-Spotify-Ads-Sdk: $SDK_PRODUCT/$PLUGIN_VERSION" and include -H "$SDK_HEADER" on all API requests.Extract the following from the user's plain-text input. If a field is missing or ambiguous, use the defaults noted below. If a required field cannot be inferred, ask the user.
| Field | Required | Default |
|---|---|---|
| name | yes | — |
| objective | yes | REACH |
Valid objectives: REACH, CLICKS, VIDEO_VIEWS, CONVERSIONS, LEAD_GEN, EVEN_IMPRESSION_DELIVERY, PODCAST_STREAMS, APP_INSTALLS, WEBSITE_VISITS
| Field | Required | Default | Notes |
|---|---|---|---|
| name | yes | — | 2-200 chars |
| start_time | yes | — | ISO 8601 UTC |
| end_time | required if LIFETIME | — | ISO 8601 UTC |
| budget.micro_amount | yes | — | Dollar amount x 1,000,000 |
| budget.type | yes | DAILY | DAILY or LIFETIME |
| asset_format | yes | AUDIO | AUDIO, VIDEO, IMAGE, or CATALOG |
| category | yes | — | Valid ADV_X_Y code (fetch from GET /ad_categories if needed) |
| bid_strategy | yes | MAX_BID | Plain string: MAX_BID, COST_PER_RESULT, AUTOBID, or UNSET |
| bid_micro_amount | yes with MAX_BID/COST_PER_RESULT | 15000000 | Bid cap in micro-units. Not required with AUTOBID. |
| pacing | no | PACING_EVEN | PACING_EVEN or PACING_ASAP |
| delivery | no | ON | ON or OFF |
| targets.age_ranges | yes | [{"min":18,"max":54}] | Array of {min, max} objects |
| targets.geo_targets | yes | {"country_code":"US"} | Flat object with country_code string |
| targets.platforms | no | ["ANDROID","DESKTOP","IOS"] | Valid: ANDROID, DESKTOP, IOS |
| targets.placements | yes | ["MUSIC"] | MUSIC or PODCAST |
| targets.genders | no | [] | MALE, FEMALE, NON_BINARY |
Ad set validation guardrails:
budget.micro_amount and bid_micro_amount must be positive when present.currency in ad set budget; currency is only required in /estimates/audience budget payloads.cost_model, skippable, is_skippable, or ad_platforms in ad set create payloads.ANDROID, DESKTOP, and IOS in targets.platforms; never use WEB, MOBILE, or CONNECTED_DEVICE.min >= 18 for age ranges unless the user explicitly confirms a market/category that allows minors.city_ids, dma_ids, postal_code_ids, region_ids), include country_code in the same geo_targets object.bid_strategy=UNSET, omit bid_micro_amount unless the API response or user-provided source explicitly requires it.| Field | Required | Notes |
|---|---|---|
| name | yes | 2-200 chars |
| tagline | yes | 2-40 chars |
| advertiser_name | yes | 2-25 chars |
| assets.asset_id | yes | UUID — prompt user to select |
| assets.logo_asset_id | yes | UUID — prompt user to select |
| assets.companion_asset_id | yes (audio) | UUID — required for AUDIO format ads |
| call_to_action.key | yes | e.g. SHOP_NOW, LEARN_MORE, LISTEN_NOW, SIGN_UP |
| call_to_action.clickthrough_url | yes | Landing page URL |
| delivery | no | ON (default) or OFF |
Before making any API calls, present the full parsed plan as a visual tree:
Campaign: "My Campaign" (objective: REACH)
├── Ad Set 1: "Ad Set A" (AUDIO, $75/day, US, ages 25-54, Mar 1 start)
│ └── Ad 1: "My Ad" → SHOP_NOW → example.com
└── Ad Set 2: "Ad Set B" (VIDEO, $500 lifetime, US, ages 18-54, Mar 4–Apr 4)
└── Ad 2: "My Video Ad" → LEARN_MORE → example.com
Also show a table with all field values for each entity. Ask the user to confirm or adjust.
If the ad category was not specified, ask the user to select one using AskUserQuestion.
You can fetch valid categories from GET /ad_categories to present options.
After the user confirms the plan but before executing API calls, run an audience estimate for each ad set's targeting:
curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST -H "Authorization: Bearer $TOKEN" \
-H "$SDK_HEADER" \
-H "Content-Type: application/json" \
-d '{
"ad_account_id": "<AD_ACCOUNT_ID>",
"start_date": "<start_time>",
"asset_format": "<AUDIO|VIDEO|IMAGE|CATALOG>",
"objective": "<campaign_objective>",
"bid_strategy": "<MAX_BID|COST_PER_RESULT|AUTOBID|UNSET>",
"bid_micro_amount": <bid>,
"budget": {"micro_amount": <budget>, "type": "<DAILY|LIFETIME>", "currency": "USD"},
"targets": { <same targets object as the ad set> }
}' \
"https://api-partner.spotify.com/ads/v3/estimates/audience"
Important: This endpoint is NOT scoped under /ad_accounts/{id}/ — it's at the top level: POST /estimates/audience. Use the base URL directly followed by /estimates/audience.
Display the estimate results in a summary:
Audience Estimate for "Ad Set A":
Projected unique users: ~142,000
Estimated daily reach: 8,500 – 12,000
Estimated daily impressions: 15,000 – 22,000
Estimated CPM: $12.50 – $18.00
Likely to deliver budget: Yes
Convert any CPM micro-amounts to dollars for display.
If the audience is too small (very low projected_unique_users or the API returns a 400 error indicating audience too small), warn the user and suggest:
Use AskUserQuestion to ask whether to:
Run the estimate for each ad set in the plan before proceeding to Step 3.
For each ad, fetch available assets from the account:
curl -s -w "\nHTTP_STATUS:%{http_code}" -H "Authorization: Bearer $TOKEN" \
-H "$SDK_HEADER" \
"$BASE_URL/ad_accounts/$AD_ACCOUNT_ID/assets?limit=50&sort_direction=DESC"
Present audio/video assets and image assets separately in tables, and ask the user to pick:
asset_format: audio for AUDIO, video for VIDEO, etc.)Execute each step in order, passing IDs forward from each response.
curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST -H "Authorization: Bearer $TOKEN" \
-H "$SDK_HEADER" \
-H "Content-Type: application/json" \
-d '{"name":"...","objective":"..."}' \
"$BASE_URL/ad_accounts/$AD_ACCOUNT_ID/campaigns"
Extract the campaign id from the response.
For each ad set:
curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST -H "Authorization: Bearer $TOKEN" \
-H "$SDK_HEADER" \
-H "Content-Type: application/json" \
-d '{
"name": "...",
"campaign_id": "<from step 4a>",
"start_time": "...",
"end_time": "...",
"budget": {"micro_amount": ..., "type": "..."},
"asset_format": "...",
"category": "ADV_X_Y",
"targets": {
"age_ranges": [{"min": ..., "max": ...}],
"geo_targets": {"country_code": "..."},
"platforms": ["ANDROID", "DESKTOP", "IOS"],
"placements": ["MUSIC"]
},
"bid_strategy": "MAX_BID",
"bid_micro_amount": ...,
"pacing": "PACING_EVEN",
"delivery": "ON"
}' \
"$BASE_URL/ad_accounts/$AD_ACCOUNT_ID/ad_sets"
Extract each ad set id for use in ad creation.
For each ad:
curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST -H "Authorization: Bearer $TOKEN" \
-H "$SDK_HEADER" \
-H "Content-Type: application/json" \
-d '{
"name": "...",
"ad_set_id": "<from step 4b>",
"tagline": "...",
"advertiser_name": "...",
"assets": {
"asset_id": "...",
"logo_asset_id": "...",
"companion_asset_id": "..."
},
"call_to_action": {
"key": "SHOP_NOW",
"clickthrough_url": "https://..."
},
"delivery": "ON"
}' \
"$BASE_URL/ad_accounts/$AD_ACCOUNT_ID/ads"
After all entities are created, display a final summary table:
| Entity | ID | Name | Status |
|---|---|---|---|
| Campaign | uuid | ... | ... |
| Ad Set 1 | uuid | ... | ... |
| ↳ Ad 1 | uuid | ... | ... |
| Ad Set 2 | uuid | ... | ... |
| ↳ Ad 2 | uuid | ... | ... |
auto_execute is true, execute each API call directly after presenting the plan.auto_execute is false, present the full plan and ask for confirmation before
executing. Then execute all calls in sequence without additional confirmation per call.HTTP_STATUS: line from curl output to determine success or failure before interpreting the response body.These are non-obvious API requirements that MUST be followed:
bid_strategy is a plain STRING enum, NOT an object. Valid: MAX_BID, COST_PER_RESULT, AUTOBID, UNSETgeo_targets is a flat object {"country_code": "US"}, NOT an array of objectsplatforms valid values are ANDROID, DESKTOP, IOS — NOT "MOBILE" or "CONNECTED_DEVICE"category is required on ad sets — must be a valid ADV_X_Y code from GET /ad_categoriesend_time is required when budget type is LIFETIMEcompanion_asset_id is required when creating ads for AUDIO ad setscall_to_action uses field name key (not type) and clickthrough_url (not url)