SuperAPI AI Directory

drafts

Coding & development

View repository

Updated 10-16 09:30

About this skill

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, 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.

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.

For reserved CONTENT or FPMNG buys, follow skills/api-reference/references/endpoints.md (Reserved Pricing and Forecasting) before presenting the plan or creating draft ad sets. Fetch the fixed rate for the planned product, dates, format, and targeting, then use its cost_micro as the draft ad set bid_micro_amount. Show the fixed rate separately from any audience estimate.

Before creating draft ad sets, run the audience estimate from build-campaign Step 2.5 for each ad set. The AUTOBID omission of bid_micro_amount applies only to ad set payloads, not to the estimate request.

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):

Set a concrete delivery_goal compatible with the parent campaign's delivery_goal_group:

delivery_goal_group Supported delivery_goal values
AWARENESS IMPRESSIONS, REACH, VIDEO_VIEWS
WEBSITE_TRAFFIC CLICKS, PAGE_VIEWS, VIDEO_VIEWS
APP_PROMOTION APP_INSTALLS
ENGAGEMENT_ON_SPOTIFY CLICKS, STREAMS, VIDEO_VIEWS
LEAD_GEN LEADS, CLICKS

Choose the value that matches the user's requested outcome and the live ad product catalog. Never set delivery_goal to UNSET or omit it when the parent campaign has a non-UNSET delivery_goal_group.

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": ...,
    "delivery_goal": "..."
  }'

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, validation_errors is 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 a validation_errors array):

  • Display each error with its entity type, entity ID, and message:
    Validation Errors:
    ✗ AD_SET (id: abc-123): Ad set targeting is required
    ✗ AD (id: def-456): Missing companion_asset_id for AUDIO format
    
  • Suggest fixes for each error
  • Ask the user if they want to fix the issues (for example, edit ad-set <draft_ad_set_id> or edit ad <draft_ad_id>) or delete the draft

If the request fails another way (no validation_errors in the body): handle it like any other error; never retry automatically.

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.
  • HTTP 400 with validation_errors — validation failed. The response is a PublishCampaignResult whose validation_errors array lists HierarchyValidationError objects.
  • Any other error — handle it like any other error; never retry automatically.

On a 400, check for validation_errors first.

Each HierarchyValidationError has:

  • validation_entity_type: CAMPAIGN, AD_SET, or AD
  • validation_entity_id: UUID of the failing entity
  • message: human-readable error description
  • error_codes: error codes for the failure

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 with validation_errors):

✗ 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>}'

On HTTP 200, display the published campaign details from the response. Published entities retain the same IDs they had as drafts.

A 400 with validation_errors means publish failed and nothing went live: show the errors, fix them with PATCH, and re-validate. Otherwise handle it like any other error; never retry automatically.


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.

  1. bid_strategy is a plain STRING enum, NOT an object. Valid: MAX_BID, COST_PER_RESULT, AUTOBID, UNSET
  2. geo_targets is a flat object {"country_code": "US"}, NOT an array of objects
  3. platforms valid values are ANDROID, DESKTOP, IOS — NOT "MOBILE" or "CONNECTED_DEVICE"
  4. category is required on ad sets — must be a valid ADV_X_Y code from GET /ad_categories
  5. end_time is required when budget type is LIFETIME
  6. companion_asset_id is required for AUDIO format ads at publish/validation time, but can be omitted when creating draft ads
  7. call_to_action uses field name key (not type) and clickthrough_url (not url)
  8. 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.
  9. Budget amounts must be in micro-units (multiply amount by 1,000,000)
  10. 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
  11. Draft ad set campaign_id must reference the draft campaign ID, not a published campaign ID
  12. Draft ad ad_set_id must reference a draft ad set ID, not a published ad set ID

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