| name | drafts |
| description | Default write workflow for Spotify Ads API campaigns, ad sets, and ads. Stage new entities or changes to published entities as drafts, validate them, and publish only after explicit confirmation. Use for create, change, update, adjust, fix, pause, resume, archive, creative, tracking, or other campaign-hierarchy writes even when the user does not say draft. |
| argument-hint | build <description> | stage-edit <campaign|ad-set|ad> <published_id> <changes> | list|get|edit|validate|publish|delete|draft-from |
| allowed-tools | ["Read","Bash","AskUserQuestion"] |
Spotify Ads API — Draft Campaign Management
Build, review, and publish campaign hierarchies using the draft workflow. Drafts are staging versions of campaigns, ad sets, and ads that can be created, edited, validated, and published as a batch — nothing goes live until you explicitly publish.
Why Drafts
The draft flow is the preferred way to create campaigns because:
- Review before going live — build the entire hierarchy, then validate and publish in one step
- Batch validation — the validate action checks the entire campaign hierarchy (campaign + ad sets + ads) at once, surfacing all errors before anything is created
- Safe iteration — edit any part of the draft hierarchy without affecting live entities
- Undo-friendly — delete drafts at any time before publishing; no cleanup of live entities needed
- Incomplete data allowed — drafts accept partial entities (e.g., an AUDIO ad without
companion_asset_id). Fields like asset_id, clickthrough_url, and tagline are optional for draft ads and only required when publishing or creating live ads. Required fields are only enforced during VALIDATE or PUBLISH, so you can build the hierarchy incrementally
The alternative (direct entity creation or updates via /campaigns, /ad_sets, /ads) writes to published entities immediately and may require permissions unavailable to some accounts or credentials. Prefer the draft flow for every campaign hierarchy create or modify request, not only complete campaign builds.
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" drafts "$@"; }
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) for use outside API calls, run api --env.
Operations
Parse the user's argument to determine the operation. For stage-edit, get, edit, delete, and draft-from, require an entity type (campaign, ad-set, or ad) with the ID. If the user provides only a bare UUID, infer the type only when the current conversation makes it unambiguous; otherwise ask which entity type they mean.
Use stage-edit automatically for ordinary requests to change a published campaign, ad set, or ad. The user does not need to mention drafts.
stage-edit <campaign|ad-set|ad> <published_id> <changes> — Safely Stage Changes to a Published Entity
This is the default operation for natural-language changes to published campaign hierarchy entities.
Step 1: Read the published entity
Use the corresponding published GET endpoint to verify the ID, capture the current values, and resolve parent IDs when needed.
Step 2: Check for an existing draft
Drafts created from published entities reuse the same entity ID. Check the corresponding draft GET endpoint before creating anything:
api GET "ad_accounts/{ad_account_id}/drafts/<campaigns|ad_sets|ads>/$ENTITY_ID"
- HTTP 200: a draft already exists. Display its current pending state and explain that the requested changes will be combined with it. Do not overwrite or discard fields that the user did not request to change.
- HTTP 404: no draft exists; continue to Step 3.
- Any other response: show the error and stop.
Step 3: Create a draft when needed
Only after a 404 from Step 2, create the draft from the published entity:
api POST "ad_accounts/{ad_account_id}/campaigns/$CAMPAIGN_ID/drafts"
api POST "ad_accounts/{ad_account_id}/ad_sets/$AD_SET_ID/drafts"
api POST "ad_accounts/{ad_account_id}/ads/$AD_ID/drafts"
Use only the endpoint matching the selected entity type. Do not send a request body.
Step 4: Patch the draft
Build a minimal patch containing only requested changes and send it to the corresponding draft endpoint:
api PATCH "ad_accounts/{ad_account_id}/drafts/campaigns/$CAMPAIGN_ID" '{...}'
api PATCH "ad_accounts/{ad_account_id}/drafts/ad_sets/$AD_SET_ID" '{...}'
api PATCH "ad_accounts/{ad_account_id}/drafts/ads/$AD_ID" '{...}'
For third_party_tracking, read and preserve every existing entry the user did not explicitly remove or replace. Send the complete intended tracking array and set measurement_event explicitly for every entry, especially CLICKED versus IMPRESSION.
Step 5: Resolve and validate the parent draft campaign
- Campaign draft: its ID is the draft campaign ID.
- Ad set draft: use its returned
campaign_id.
- Ad draft: fetch its draft ad set by
ad_set_id, then use the ad set's campaign_id.
Fetch that draft campaign immediately before validation, use its current draft_hierarchy_version, and run VALIDATE. Report the change as staged, including the affected entity ID and validation result.
Do not publish as part of stage-edit. Publishing is a separate operation that always requires explicit confirmation immediately before the request.
build — Create a Full Draft Campaign Hierarchy
Given a plain-text campaign description, create the full draft hierarchy: draft campaign → draft ad sets → draft ads. This mirrors /spotify-ads-api:build-campaign but creates drafts instead of live entities.
Step 1: Parse the Campaign Description
Extract fields exactly as documented in the build-campaign skill. The same field requirements, defaults, and validation guardrails apply (micro-amounts, bid_strategy as plain string, geo_targets as flat object, platform enums, etc.).
Step 1.5: Load Ad Product Rules
Read and follow
$PLUGIN_ROOT/skills/api-reference/references/ad-product-validation.md. Fetch the live
catalog once for this draft-build workflow, resolve the planned campaign product, and
use the applicable rules while constructing the draft plan. Do not display a per-field
checklist.
Step 2: Confirm the Parsed Plan
Present the plan as a visual tree, clearly labeled as DRAFT:
DRAFT Campaign: "My Campaign" (AWARENESS)
├── DRAFT Ad Set 1: "Ad Set A" (AUDIO, $75/day, US, ages 25-54, Mar 1 start)
│ └── DRAFT Ad 1: "My Ad" → SHOP_NOW → example.com
└── DRAFT Ad Set 2: "Ad Set B" (VIDEO, $500 lifetime, US, ages 18-54, Mar 4–Apr 4)
└── DRAFT Ad 2: "My Video Ad" → LEARN_MORE → example.com
Ask the user to confirm or adjust before creating drafts.
Step 3: Prompt for Assets
Fetch available assets from the account and present them for selection, just like the build-campaign skill:
api GET "ad_accounts/{ad_account_id}/assets?limit=50&sort_direction=DESC"
Step 3.5: Validate Against Ad Product Rules
Using the catalog loaded in Step 1.5, validate the complete draft hierarchy now that
assets and dependent fields are known. Apply resolvable runtime checks as well as
static rules.
Do not print per-field successes or add another confirmation. Add a compact validation
status to the existing draft plan, and interrupt only for an explicit incompatible user
choice or when no safe compliant value can be inferred. Catalog preflight does not
replace the draft hierarchy VALIDATE action in Step 5.
Step 4: Create Draft Entities Sequentially
4a. Create Draft Campaign:
api POST "ad_accounts/{ad_account_id}/drafts/campaigns" \
'{"name":"...","delivery_goal_group":"..."}'
Map the user's campaign goal to delivery_goal_group:
| User goal | delivery_goal_group |
|---|
| Awareness, reach, brand recall, even impression delivery | AWARENESS |
| Website traffic, clicks, website visits | WEBSITE_TRAFFIC |
| App installs or mobile app promotion | APP_PROMOTION |
| Video views, podcast streams, or on-platform engagement | ENGAGEMENT_ON_SPOTIFY |
| Lead generation | LEAD_GEN |
Do not set the deprecated objective field on draft campaigns.
Extract the draft campaign id from the response. The response includes an initial draft_hierarchy_version, but do not rely on that value after creating child draft ad sets or ads because any hierarchy edit can increment the version.
4b. Create Draft Ad Sets (using campaign_id = draft campaign ID from 4a):
api POST "ad_accounts/{ad_account_id}/drafts/ad_sets" \
'{
"campaign_id": "<draft_campaign_id from step 4a>",
"name": "...",
"start_time": "...",
"end_time": "...",
"budget": {"micro_amount": ..., "type": "..."},
"asset_format": "...",
"category": "ADV_X_Y",
"targets": { ... },
"bid_strategy": "MAX_BID",
"bid_micro_amount": ...
}'
Extract each draft ad set id.
4c. Create Draft Ads (using ad_set_id = draft ad set ID from 4b):
api POST "ad_accounts/{ad_account_id}/drafts/ads" \
'{
"ad_set_id": "<draft_ad_set_id from step 4b>",
"name": "...",
"tagline": "...",
"advertiser_name": "...",
"assets": {
"asset_id": "...",
"logo_asset_id": "...",
"companion_asset_id": "..."
},
"call_to_action": {
"key": "SHOP_NOW",
"clickthrough_url": "https://..."
},
"third_party_tracking": [
{"measurement_event": "IMPRESSION", "measurement_partner": "DCM", "url": "https://...trackimp/..."},
{"measurement_event": "CLICKED", "measurement_partner": "DCM", "url": "https://...trackclk/..."}
]
}'
Step 5: Validate the Draft
After all draft entities are created, fetch the draft campaign again to get the current draft_hierarchy_version, then automatically run validation. Do not reuse a version captured before child draft ad sets or ads were created.
api GET "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID"
api POST "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID" \
'{"action":"VALIDATE","draft_hierarchy_version":<version>}'
The draft_hierarchy_version must match the current value from the draft campaign response.
If validation succeeds (HTTP 200 with validation_errors: null):
- Display a success summary with the full draft hierarchy
- Ask the user: Publish now or Keep as draft for later
If validation returns errors (HTTP 400 with validation_errors array):
Step 6: Summary
Display a final summary:
| Entity | Draft ID | Name | Status |
|---|
| Draft Campaign | uuid | ... | DRAFT |
| Draft Ad Set 1 | uuid | ... | DRAFT |
| ↳ Draft Ad 1 | uuid | ... | DRAFT |
Include the draft_hierarchy_version and remind the user they can:
- Validate:
/spotify-ads-api:drafts validate <draft_campaign_id>
- Publish:
/spotify-ads-api:drafts publish <draft_campaign_id>
- Edit:
/spotify-ads-api:drafts edit <campaign|ad-set|ad> <draft_id>
list — List Drafts
List draft entities. Argument specifies which type: campaigns, ad-sets, or ads. Default to campaigns.
List draft campaigns:
api GET "ad_accounts/{ad_account_id}/drafts/campaigns?limit=50&sort_direction=DESC"
Format as table: Draft ID | Name | Status | Delivery Goal Group | Version | Created
Optional filters use the actual query parameter names: campaign_ids (repeated param), channel, statuses (repeated param), sort_field, sort_direction.
List draft ad sets:
api GET "ad_accounts/{ad_account_id}/drafts/ad_sets?limit=50"
Format as table: Draft ID | Name | Campaign ID | Status | Format | Budget
Optional filters: campaign_ids (repeated param), statuses (repeated param).
List draft ads:
api GET "ad_accounts/{ad_account_id}/drafts/ads?limit=50"
Format as table: Draft ID | Name | Ad Set ID | Status
Optional filters: ad_set_ids (repeated param), statuses (repeated param).
get <campaign|ad-set|ad> <draft_id> — Get a Draft by ID
Use the entity type from the command to select the endpoint. If the user supplies only a bare ID, infer the type only when prior context is unambiguous; otherwise ask the user for campaign, ad-set, or ad.
Draft campaign:
api GET "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID"
Draft ad set:
api GET "ad_accounts/{ad_account_id}/drafts/ad_sets/$DRAFT_AD_SET_ID"
Draft ad:
api GET "ad_accounts/{ad_account_id}/drafts/ads/$DRAFT_AD_ID"
Display all fields in a readable format. Note that draft_hierarchy_version is only populated on campaign drafts; ad set and ad drafts return null for this field.
edit <campaign|ad-set|ad> <draft_id> — Update a Draft
Use the entity type from the command to select the endpoint, then prompt the user for fields to update. The same field validations as create apply.
Before the PATCH, read and follow
$PLUGIN_ROOT/skills/api-reference/references/ad-product-validation.md. Fetch the live
catalog and current draft entity, then traverse its actual parent chain: draft ad →
draft ad set → draft campaign, or draft ad set → draft campaign. Deep-merge the PATCH
into the current entity and validate the effective result against the matching entity's
update (when present) plus both rules. Do not print a checklist or add another
confirmation.
Update draft campaign:
api PATCH "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID" \
'{"name":"...","delivery_goal_group":"..."}'
Updatable campaign fields: name, purchase_order, delivery_goal_group, status. Do not introduce the deprecated objective field when editing drafts.
Update draft ad set:
api PATCH "ad_accounts/{ad_account_id}/drafts/ad_sets/$DRAFT_AD_SET_ID" \
'{...}'
Updatable ad set fields: name, start_time, end_time, budget, bid_micro_amount, bid_strategy, targets, pacing, asset_format, category, frequency_caps, cost_model, delivery_goal, promotion, video_delivery_formats, status.
Update draft ad:
api PATCH "ad_accounts/{ad_account_id}/drafts/ads/$DRAFT_AD_ID" \
'{...}'
Updatable ad fields: name, advertiser_name, tagline, assets, asset_format, call_to_action, third_party_tracking, placements, weight, status.
After updating, display the updated draft. For campaign drafts, note the new draft_hierarchy_version. For ad set and ad drafts, draft_hierarchy_version is null in the response — fetch the parent draft campaign to get the updated version.
validate <draft_campaign_id> — Validate a Draft Campaign
Dry-run the publish to check for errors across the entire hierarchy (campaign + all ad sets + all ads):
api POST "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID" \
'{"action":"VALIDATE","draft_hierarchy_version":<version>}'
First, fetch the draft campaign to get the current draft_hierarchy_version:
api GET "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID"
Handling the response:
- HTTP 200 — validation passed. The response is a
PublishCampaignResult with validation_errors: null and optionally campaign data.
- HTTP 400 — validation failed. The response contains a
validation_errors array of HierarchyValidationError objects.
Each HierarchyValidationError has:
validation_entity_type: CAMPAIGN, AD_SET, or AD
validation_entity_id: UUID of the failing entity
message: human-readable error description
If no validation errors (HTTP 200):
✓ Draft campaign "My Campaign" passed validation.
Campaign + 2 ad sets + 3 ads are ready to publish.
Publish now? /spotify-ads-api:drafts publish <draft_campaign_id>
If validation errors exist (HTTP 400):
✗ Draft campaign "My Campaign" has validation errors:
Entity | ID | Error
AD_SET | abc-123 | Budget micro_amount is required
AD | def-456 | Missing companion_asset_id for AUDIO format
AD | ghi-789 | call_to_action.clickthrough_url is required
Fix these issues with: /spotify-ads-api:drafts edit <campaign|ad-set|ad> <draft_id>
Then re-validate with: /spotify-ads-api:drafts validate <draft_campaign_id>
publish <draft_campaign_id> — Publish a Draft Campaign
Publish the entire draft hierarchy (campaign + ad sets + ads) as live entities. Published entities retain the same IDs they had as drafts. Always validate first before publishing.
Step 1: Fetch the draft campaign to get draft_hierarchy_version
api GET "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID"
Step 2: Run validation first
api POST "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID" \
'{"action":"VALIDATE","draft_hierarchy_version":<version>}'
If validation errors exist, display them and stop. Do not publish with validation errors.
Step 3: Confirm with the user
Show the full draft hierarchy that will be published and ask for confirmation:
Ready to publish draft campaign "My Campaign":
├── Ad Set 1: "Ad Set A" (AUDIO, $75/day)
│ └── Ad 1: "My Ad"
└── Ad Set 2: "Ad Set B" (VIDEO, $500 lifetime)
└── Ad 2: "My Video Ad"
This will create live entities. Proceed?
Step 4: Publish
Fetch the draft campaign again immediately before publishing. If the draft_hierarchy_version differs from the version that just passed validation, re-run validation before publishing.
api GET "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID"
api POST "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID" \
'{"action":"PUBLISH","draft_hierarchy_version":<version>}'
Display the published campaign details from the response. Published entities retain the same IDs they had as drafts.
delete <campaign|ad-set|ad> <draft_id> — Delete a Draft
Use the entity type from the command to select the endpoint. If the user supplies only a bare ID, infer the type only when prior context is unambiguous; otherwise ask.
Delete draft campaign (also removes associated draft ad sets and ads):
api DELETE "ad_accounts/{ad_account_id}/drafts/campaigns/$DRAFT_CAMPAIGN_ID"
Delete draft ad set:
api DELETE "ad_accounts/{ad_account_id}/drafts/ad_sets/$DRAFT_AD_SET_ID"
Delete draft ad:
api DELETE "ad_accounts/{ad_account_id}/drafts/ads/$DRAFT_AD_ID"
Expect a 204 No Content response on success.
draft-from <campaign|ad-set|ad> <entity_id> — Create a Draft from a Published Entity
Create a draft copy of an existing live campaign, ad set, or ad for editing. Use the entity type from the command to select the endpoint. This is useful for making changes to published entities via the draft → edit → validate → publish workflow.
Draft from published campaign:
api POST "ad_accounts/{ad_account_id}/campaigns/$CAMPAIGN_ID/drafts"
Draft from published ad set:
api POST "ad_accounts/{ad_account_id}/ad_sets/$AD_SET_ID/drafts"
Draft from published ad:
api POST "ad_accounts/{ad_account_id}/ads/$AD_ID/drafts"
The draft-from-published response reuses the same ID as the live entity (not a new UUID). The status becomes ACTIVE_RESTRICTED.
For a campaign draft, the returned entity is the draft campaign used for validate/publish. For an ad set draft, use its campaign_id as the draft campaign ID for validate/publish. For an ad draft, fetch the draft ad set referenced by its ad_set_id, then use that draft ad set's campaign_id as the draft campaign ID.
Display the created draft and suggest next steps:
- Edit:
/spotify-ads-api:drafts edit <campaign|ad-set|ad> <draft_id>
- Validate:
/spotify-ads-api:drafts validate <draft_campaign_id>
- Publish:
/spotify-ads-api:drafts publish <draft_campaign_id>
Critical Schema Notes
Unlike direct entity creation, draft creation accepts incomplete data — required fields are only enforced during VALIDATE or PUBLISH actions. This is a key benefit of drafts: save work in progress and complete it later.
bid_strategy is a plain STRING enum, NOT an object. Valid: MAX_BID, COST_PER_RESULT, AUTOBID, UNSET
geo_targets is a flat object {"country_code": "US"}, NOT an array of objects
platforms 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_categories
end_time is required when budget type is LIFETIME
companion_asset_id is required for AUDIO format ads at publish/validation time, but can be omitted when creating draft ads
call_to_action uses field name key (not type) and clickthrough_url (not url)
third_party_tracking uses field measurement_event (NOT type) to distinguish tracker categories. Valid values: IMPRESSION, CLICKED, START, FIRST_QUARTILE, MIDPOINT, THIRD_QUARTILE, COMPLETE, VIEWABLE_IMPRESSION. If measurement_event is omitted, it defaults to IMPRESSION — always set it explicitly. Use CLICKED for click trackers and IMPRESSION for impression trackers.
- Budget amounts must be in micro-units (multiply amount by 1,000,000)
draft_hierarchy_version is required when publishing or validating — always fetch the current version from the draft campaign immediately before calling publish/validate; never reuse a version captured before child drafts or edits. This field is only populated on draft campaign responses; ad set and ad draft responses return null. The version on the campaign increments when any entity in the hierarchy is created or edited
Execution Behavior
- If
auto_execute is true, execute read, create, edit, delete, and validate calls directly after any required planning or asset-selection confirmation.
- If
auto_execute is false, present the curl command to the user and ask for confirmation before executing.
- Never auto-execute
PUBLISH. Publishing creates live entities and always requires explicit user confirmation immediately before the publish request.
- 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.
- Draft DELETE is safe to retry — unlike POST/PATCH, DELETE on drafts is idempotent.
- If an explicitly requested direct/live campaign hierarchy write fails with HTTP 403 or an edit-permission error, describe it as a denial of direct editing only. Do not infer that the credentials are entirely read-only or identify a specific organizational role. Offer
stage-edit as the compatible alternative.
Draft vs Direct Entity Creation
| Aspect | Draft Flow (Preferred) | Direct Flow |
|---|
| Validation | Batch — validate entire hierarchy at once | Per-entity — errors discovered one at a time |
| Going live | Explicit publish step | Immediate on creation |
| Editing | Free — edit any draft entity before publish | Requires PATCH on live entities |
| Undo | Delete the draft — no cleanup needed | Must archive/pause live entities |
| Use case | Default for new entities and changes to published entities | Only explicit immediate/direct live writes |