Spotify Ads API — Ad Sets & Ads Management
Manage ad sets and ads via the Spotify Ads API. Read settings from the active platform settings file.
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" ads "$@"; }
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
The argument format is: <resource> <operation> [id]
- Resource:
ad-setsorads - Operation:
list,create,get,update - If no argument, ask which resource and operation.
For create and update, default to draft endpoints even when the user does not mention drafts. create-live and update-live are explicit escape hatches for direct published writes.
Ad Set Operations
ad-sets list
api GET "ad_accounts/{ad_account_id}/ad_sets?limit=50&sort_direction=DESC"
Format as table: ID | Name | Campaign ID | Status | Format | Budget | Start
ad-sets create
Collect the required fields below first. Before the POST, read and follow
$PLUGIN_ROOT/skills/api-reference/references/ad-product-validation.md. Fetch the live
catalog, parent campaign, and any runtime inputs required by its rules, then validate
the final ad set body against ad_set.create plus ad_set.both. Do not add a separate
validation confirmation.
Prompt for required fields:
- name (2-200 chars)
- campaign_id (uuid — suggest listing campaigns first)
- start_time (ISO 8601 datetime)
- end_time (ISO 8601 — required if budget type is LIFETIME)
- budget — ask for amount in the ad account's billing currency and type (DAILY/LIFETIME), convert to micro_amount
- asset_format (AUDIO, VIDEO, IMAGE, CATALOG)
- category (required — valid
ADV_X_Ycode, fetch fromGET /ad_categoriesif needed) - targets — ask for targeting preferences:
- Age range (e.g., 18-34) →
"age_ranges": [{"min": 18, "max": 34}] - Geo targeting — see detailed instructions below
- Genders (optional) →
"genders": ["MALE", "FEMALE", "NON_BINARY"] - Platforms (optional) →
"platforms": ["ANDROID", "DESKTOP", "IOS"](NOT "MOBILE" or "CONNECTED_DEVICE") - Placements (required) →
"placements": ["MUSIC"]
- Age range (e.g., 18-34) →
- bid_strategy — plain string:
MAX_BID,COST_PER_RESULT,AUTOBID, orUNSET. Default toMAX_BID. - bid_micro_amount (required with MAX_BID or COST_PER_RESULT, not required with AUTOBID) — ask for the bid cap in the ad account's billing currency, convert to micro-amount. This is the maximum CPM. Example: $15 USD =
15000000, ¥160 JPY =160000000
Important: Convert amounts to micro-amounts by multiplying by 1,000,000. This applies to both budget.micro_amount and bid_micro_amount.
Ad set validation guardrails before any POST:
- Never send zero or negative
budget.micro_amount; ask for a positive budget and convert it to micro-units. - Never send
bid_micro_amount: 0withMAX_BIDorCOST_PER_RESULT; ask for a positive bid cap. - Do not send
bid_micro_amountwithbid_strategy=UNSETunless the API response or user-provided source explicitly requires it. - Keep
budgettomicro_amountandtype; do not includecurrencyon ad set create payloads. - Valid
targets.platformsvalues are onlyANDROID,DESKTOP, andIOS; never sendWEB,MOBILE,CONNECTED_DEVICE, orad_platforms. - Do not send
cost_model,skippable,is_skippable, orad_platformsin ad set create payloads. - Use age ranges with
min >= 18unless the user has explicitly confirmed a market/category that allows minors. - If using
city_ids,postal_code_ids, orregion_ids, include the parentcountry_codein the samegeo_targetsobject.
Geo-Targeting
Structure: geo_targets is a flat object (NOT an array) with a required country_code and optional refinement arrays.
Lookup Geo IDs: Use the /targets/geos endpoint to find geo IDs:
# Search by location name
api GET "targets/geos?country_code=US&q=Connecticut&limit=20"
# Search by postal code
api GET "targets/geos?country_code=US&q=06103&limit=20"
Response includes id, type, name, and parent_geo_name for each geo.
Geo Types:
REGION— States/provinces (e.g., Connecticut, California, Ontario)DMA_REGION— Designated Market Areas for media targeting (e.g., "Hartford & New Haven, CT"). Note: DMA-level targeting viadma_idsis no longer supported. DMAs can still be looked up but cannot be used as targeting refinements.CITY— Cities and townsPOSTAL_CODE— ZIP codes (format: "US:06103", "CA:M5H")
Targeting Examples:
- Country-level (broadest):
"geo_targets": {
"country_code": "US"
}
- State/Region-level:
"geo_targets": {
"country_code": "US",
"region_ids": ["4831725"] // Connecticut
}
- City-level:
"geo_targets": {
"country_code": "US",
"city_ids": ["4845411", "5284283"] // West Hartford, Colchester
}
- Postal code-level (most granular):
"geo_targets": {
"country_code": "US",
"postal_code_ids": ["US:06103", "US:06105"]
}
- Multi-level (combine different geo types):
"geo_targets": {
"country_code": "US",
"region_ids": ["4831725"], // Connecticut
"city_ids": ["4845411"] // West Hartford
}
Workflow:
- Ask user for geo preference (e.g., "Connecticut", "Hartford DMA", "West Hartford")
- Call
/targets/geoswith user's query - Display results with type, name, and parent location
- Let user select from results or refine search
- Build
geo_targetsobject with appropriate IDs - NEVER fall back to country-only without asking user first
Pre-flight audience estimate: Before executing the POST, run an audience estimate to validate targeting:
api POST "estimates/audience" \
'{
"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 as above> }
}'
Note: 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 summary:
Audience Estimate:
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
If the audience is too small (low projected users or 400 error), warn the user and suggest:
- Broadening the age range
- Adding more platforms
- Switching from VIDEO to AUDIO format (lower thresholds)
- Expanding geo targeting
Ask whether to proceed, adjust targeting, or cancel before creating the ad set.
Create the draft ad set:
The campaign_id must reference a draft campaign. If the supplied campaign is published, first check for and reuse its draft or create one with POST /campaigns/{campaign_id}/drafts.
api POST "ad_accounts/{ad_account_id}/drafts/ad_sets" \
'{...}'
ad-sets get <id>
api GET "ad_accounts/{ad_account_id}/ad_sets/$AD_SET_ID"
ad-sets update <id>
Before the PATCH, read and follow
$PLUGIN_ROOT/skills/api-reference/references/ad-product-validation.md. Fetch the live
catalog, current ad set, parent campaign, and runtime state required by applicable
rules. Deep-merge the proposed PATCH into the current ad set and validate the effective
entity against ad_set.update plus ad_set.both. Do not add a separate validation
confirmation.
Prompt for fields to update (min 1). Same fields as create, all optional.
Read the published ad set, then check GET /drafts/ad_sets/{id}. If it returns 404, create a draft with POST /ad_sets/{id}/drafts. If a draft already exists, disclose its pending state before combining changes. PATCH /drafts/ad_sets/{id}, resolve its draft campaign_id, fetch the draft campaign's current hierarchy version, and validate it. Keep the result staged.
Ad Operations
ads list
api GET "ad_accounts/{ad_account_id}/ads?limit=50&sort_direction=DESC"
Format as table: ID | Name | Ad Set ID | Status | Delivery
ads create
Collect the required fields and asset selections below first. Before the POST, read and
follow $PLUGIN_ROOT/skills/api-reference/references/ad-product-validation.md. Fetch
the live catalog, current parent ad set and campaign, and referenced assets, then
validate the final ad body against ad.create plus ad.both. Do not add a separate
validation confirmation.
Prompt for required fields:
- name (2-200 chars)
- ad_set_id (uuid — suggest listing ad sets first)
- tagline (2-40 chars, ad headline; required for live ads, optional for drafts)
- advertiser_name (2-25 chars)
- assets — fetch available assets from
GET /assetsand prompt user to select:asset_id(required for live ads, optional for drafts — audio/video/image creative matching ad set format)logo_asset_id(required — logo image)companion_asset_id(required for AUDIO format — companion image)
- call_to_action — uses field
key(NOTtype) andclickthrough_url(NOTurl):key: SHOP_NOW, LEARN_MORE, LISTEN_NOW, SIGN_UP, WATCH_NOW, BUY_NOW, DOWNLOAD, etc.clickthrough_url: landing page URL (required for live ads, optional for drafts)
- delivery (ON/OFF, default ON)
- third_party_tracking (optional) — array of tracking pixels. Each entry needs:
measurement_event: required —IMPRESSION,CLICKED,START,FIRST_QUARTILE,MIDPOINT,THIRD_QUARTILE,COMPLETE, orVIEWABLE_IMPRESSION. If omitted, defaults to IMPRESSION — always set explicitly, especially for click trackers.measurement_partner:DCM,IAS,MOAT,DOUBLEVERIFY, orUNSETurl: the tracking pixel URL- Example with both impression and click trackers:
"third_party_tracking": [ {"measurement_event": "IMPRESSION", "measurement_partner": "DCM", "url": "https://ad.doubleclick.net/ddm/trackimp/..."}, {"measurement_event": "CLICKED", "measurement_partner": "DCM", "url": "https://ad.doubleclick.net/ddm/trackclk/..."} ]
api POST "ad_accounts/{ad_account_id}/drafts/ads" \
'{...}'
The ad_set_id must reference a draft ad set. If the supplied ad set is published, first check for and reuse its draft or create one with POST /ad_sets/{ad_set_id}/drafts.
ads get <id>
api GET "ad_accounts/{ad_account_id}/ads/$AD_ID"
ads update <id>
Before the update, read and follow
$PLUGIN_ROOT/skills/api-reference/references/ad-product-validation.md. Fetch the live
catalog, current ad, parent ad set and campaign, and any referenced assets. Deep-merge
the proposed changes into the current ad and validate the effective entity against
ad.update (when present) plus ad.both. Do not add a separate validation
confirmation.
Read the published ad, then check GET /drafts/ads/{id}. If it returns 404, create a draft with POST /ads/{id}/drafts. If a draft already exists, disclose its pending state before combining changes. PATCH /drafts/ads/{id}, fetch its parent draft ad set to resolve the draft campaign, fetch the campaign's current hierarchy version, and validate it. Keep the result staged.
Draft ad updates support name, advertiser_name, tagline, assets, asset_format, call_to_action, third_party_tracking, placements, weight, and status. Always preserve third-party tracking entries the user did not explicitly remove or replace, and set measurement_event explicitly on every entry.
Explicit direct writes
Use ad-sets create-live, ad-sets update-live <id>, ads create-live, or ads update-live <id> only when the user explicitly requests an immediate/direct change to a published entity or asks to skip drafts.
If a direct write returns HTTP 403 or an edit-permission error, do not retry it and do not conclude that the credentials are entirely read-only. State that direct editing of the published entity was denied and offer draft staging instead.
Execution Behavior
- If
auto_executeistrue, execute directly. - If
auto_executeisfalse, present the curl command and ask for confirmation. - Display responses in readable format.
- 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. Never automatically retry POST or PATCH requests — they may have succeeded server-side despite an error response.
- When converting budgets, always confirm the micro-amount with the user (e.g., "$50/day = 50,000,000 micro-amount").
- Treat a 404 from a draft existence check as "no draft exists"; for any other error, stop and show the response.
- Never publish a draft without a separate user request and explicit confirmation immediately before
PUBLISH.