Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ These non-obvious API quirks were discovered through real testing and are critic
- **Tracking headers**: The request wrapper (`scripts/api-request.sh`) injects `X-Spotify-Ads-Sdk: <sdk-product>/<version>` and `X-Spotify-Ads-Skill: <skill-name>` on every API call. Skills never construct these headers manually — the wrapper reads the plugin version from the platform manifest and takes the skill name as its first argument. This eliminates malformed-header errors seen in production logs (e.g., doubled `X-Spotify-Ads-Sdk: X-Spotify-Ads-Sdk: ...`).
- **`entity_status_type` must match `entity_type`** in `aggregate_reports` queries. For example, use `entity_status_type=AD_SET` when `entity_type=AD_SET` — using `entity_status_type=CAMPAIGN` with `entity_type=AD_SET` causes a filter validation error.
- **Audience estimates**: The build-campaign and ads skills run `POST /estimates/audience` before creating ad sets to validate targeting. This catches "min audience threshold" errors before they happen.
- **Ad product catalog validation**: Before every campaign, ad set, or ad POST/PATCH, follow `skills/api-reference/references/ad-product-validation.md`. Fetch `GET /ad_product_catalog` once per workflow; do not maintain a timed cross-workflow cache because the endpoint returns `Cache-Control: no-cache, no-store`. Validate final creates and deep-merged effective updates, including required parent/runtime context. Never send a known violation, but do not add per-field success checklists or validation-only confirmation gates. Campaign responses do not consistently expose `ad_product`, so use known workflow context and do not silently infer a reserved CONTENT/FPMNG product.

## OpenAPI Spec

Expand Down
6 changes: 3 additions & 3 deletions agents/spotify-ads-request-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ You are a Spotify Ads API specialist that translates natural language advertisin
- Date descriptions ("last month", "next week") → ISO 8601 datetimes
- Status changes ("pause", "stop", "archive") → status field values
4. Identify any missing required fields and ask the user via AskUserQuestion
5. Construct the `api()` helper call(s) with the correct method, path, and JSON body

6. Before creating any ad set, run a pre-flight audience estimate using `POST /estimates/audience` (top-level endpoint, NOT under `/ad_accounts/{id}/`) with the proposed targeting parameters. Display the estimated reach and impressions. If the audience is too small or the estimate indicates delivery issues, warn the user and suggest targeting adjustments before proceeding.
5. Construct the `api()` helper call(s) with the correct method, path, and JSON body.
6. Before any campaign, ad set, or ad POST/PATCH, read and follow `$PLUGIN_ROOT/skills/api-reference/references/ad-product-validation.md`. Fetch `GET /ad_product_catalog` once for the current workflow, validate final creates or deep-merged effective updates, and never send a known catalog violation. Do not print per-field success checklists or add a validation-only confirmation; surface only incompatible explicit choices or unresolved material issues.
7. Before creating any ad set, run a pre-flight audience estimate using `POST /estimates/audience` (top-level endpoint, NOT under `/ad_accounts/{id}/`) with the proposed targeting parameters. Display the estimated reach and impressions. If the audience is too small or the estimate indicates delivery issues, warn the user and suggest targeting adjustments before proceeding.

**Dashboard Routing:**
When the user asks about campaign performance, summaries, or dashboard-like views (e.g., "How are my campaigns doing?", "Show me a summary of my ad performance", "What's my spend today?", "Campaign dashboard", "Quick overview of all campaigns"), route them to the `/spotify-ads-api:dashboard` skill.
Expand Down
27 changes: 27 additions & 0 deletions skills/ads/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ 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)
Expand Down Expand Up @@ -189,6 +195,14 @@ 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.
Expand All @@ -202,6 +216,12 @@ 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)
Expand Down Expand Up @@ -240,6 +260,13 @@ 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.
Expand Down
2 changes: 2 additions & 0 deletions skills/api-reference/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ For changes to published campaigns, ad sets, or ads, first check whether a same-
- `GET /businesses/{id}` — Get business by ID
- `GET /targets/artists` — Search artist targets
- `GET /ad_categories` — List ad categories
- `GET /ad_product_catalog` — Get live product-specific validation rules for AUCTION, CONTENT, and FPMNG. Fetch once per create or update workflow and do not retain a timed cross-operation cache. Follow `references/ad-product-validation.md` before mutating campaigns, ad sets, or ads.
- `POST /estimates/audience` — Estimate audience size for targeting parameters (recommended before creating ad sets to validate reach)
- `POST /estimates/bid` — Get bid recommendations
- `POST /ad_accounts/{id}/reserved_prices` — Get pricing for reserved ad products (fCPM)
Expand Down Expand Up @@ -222,6 +223,7 @@ For detailed request/response schemas and field definitions, consult:
- **`references/endpoints.md`** — Complete endpoint details with all parameters and response schemas
- **`references/schemas.md`** — Request/response body schemas with field types, constraints, and required fields
- **`references/enums.md`** — All enum values for status fields, asset formats, targeting options, report dimensions/metrics
- **`references/ad-product-validation.md`** — Mandatory catalog-validation procedure for campaign, ad set, and ad mutations

### Example Files

Expand Down
113 changes: 113 additions & 0 deletions skills/api-reference/references/ad-product-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Ad Product Catalog Validation

Use this procedure before every campaign, ad set, or ad `POST` or `PATCH`, including
draft creation and edits, clone operations, bulk changes, and replacement ads.

The hard invariant is: **never send a request with a known, unresolved ad product
catalog violation.** Printing a checklist is not validation and is not required.

## 1. Fetch Rules Once Per Workflow

Fetch the catalog at the start of the current create or update workflow:

```bash
api GET "ad_product_catalog"
```

Reuse that response while assembling and executing the current workflow. Do not reuse
a response retained from an earlier operation or attempt to maintain a timed session
cache. If the catalog request fails, do not execute a mutation that depends on it.

The catalog layers product-specific restrictions on top of the OpenAPI schema:

- OpenAPI defines request shapes and field types.
- The catalog defines product-specific allowed values, required or forbidden fields,
constraints, restrictions, frequency caps, and cross-field rules.
- For creates, apply the matching entity's `create` and `both` sections.
- For updates, apply the matching entity's `update` and `both` sections. A missing
operation section means there are no additional rules in that section; `both` still
applies.

When the catalog explicitly lists product-specific allowed values, use that live list
for product validation; some deprecated enums in the committed OpenAPI may lag the
catalog. OpenAPI still governs whether the field exists and what shape and type it has.
If the catalog requires a field or shape that OpenAPI cannot represent, do not invent a
payload or claim that validation passed. Stop before the mutation and explain the
conflict.

## 2. Resolve the Ad Product

- For campaign creation, use the request's `ad_product`. Treat an omitted value,
`UNSET`, or `UNKNOWN` as `AUCTION`.
- For a hierarchy created in the current workflow, carry that resolved product forward
to its ad sets and ads; do not refetch the campaign merely to rediscover it.
- For existing ad sets and ads, fetch the entity chain needed to reach the parent
campaign. Use `ad_product` when the campaign response provides it.
- When an existing campaign response omits `ad_product`, use `AUCTION` only when the
request context and entity data do not indicate a reserved `CONTENT` or `FPMNG`
campaign. If the user, source operation, pricing fields, or entity configuration
indicates a reserved product, obtain an authoritative product choice instead of
silently defaulting to `AUCTION`.
- Do not infer a reserved product solely from a campaign objective.

For bulk operations, resolve each distinct campaign once and group entities by resolved
product. For clones, validate against the product that the new campaign request will
actually create, not merely the source campaign's product.

## 3. Validate the Final Effective Entity

Assemble all fields before validation.

- **Create:** validate the final request body plus any parent or asset data referenced
by cross-entity rules.
- **Update:** fetch the current entity, deep-merge the proposed PATCH into it, and
validate the resulting effective entity. Do not validate only the changed fields.
- On update, apply rules conditioned on a value being new or changed only when the
PATCH actually changes that value. Do not reject a valid historical entity by
reapplying a creation-time future-date check to an unchanged start time.
- Fetch enough parent context to evaluate catalog rules. Ad validation can require the
parent ad set's format, platforms, and dates as well as the campaign objective or
delivery goal group.
- Fetch referenced assets when rules depend on asset type, status, duration, audio
tracks, or archive state.

Do not manufacture a pass result for fields or conditions that the catalog does not
address.

## 4. Handle Static and Runtime Rules Honestly

Classify applicable rules while validating:

1. **Static rules** can be evaluated from the final entity and fetched parent context.
Enforce these before the mutation.
2. **Resolvable runtime rules** require a read-only API check. Use the appropriate
endpoint when the inputs are available, for example audience or bid estimates,
reserved pricing, asset lookup, or reporting data needed for a budget decrease.
3. **Server-only rules** depend on state the public API does not expose, such as an
internal exemption or cooldown. Do not label these as passed. Apply every known
prerequisite, allow the mutation endpoint to perform the authoritative check, and
report any rejection normally.

If a known value violates a rule, do not send it. If a runtime condition cannot be
proved locally, do not turn that uncertainty into a user confirmation gate or claim
that the condition passed.

## 5. Minimize User Interruptions

- If an assistant-inferred value or default violates the catalog, replace it with a
compliant value and disclose the adjustment in the existing plan or change summary.
- If a value explicitly chosen by the user violates the catalog, explain the exact
rule, recommend compliant alternatives, and ask one focused question. Revalidate the
revised value.
- Ask only when there is no safe compliant choice or the alternatives materially change
the user's intent.
- Do not print per-field pass checklists or add a separate confirmation for validation.
When the workflow already presents a plan or change summary, add one compact line such
as `Ad product validation: static AUCTION rules passed` and mention only material
adjustments or unresolved server-only checks.
- Validation never replaces an existing confirmation required by `auto_execute`, bulk
changes, or draft publishing.

For draft hierarchies, perform this catalog preflight before creating or editing drafts,
then run the draft campaign `VALIDATE` action as the authoritative hierarchy check.
Publishing remains the only mandatory extra confirmation.
94 changes: 94 additions & 0 deletions skills/api-reference/references/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,100 @@ Create a new ad account under a business.

---

## Ad Product Catalog

### GET /ad_product_catalog
Returns the live validation rules for externally available ad products: AUCTION,
CONTENT, and FPMNG. These product-specific rules layer on top of the OpenAPI request
shape and field types.

Fetch the catalog once for each create or update workflow and reuse it only during that
workflow. Do not maintain a timed session cache; the endpoint response is delivered
with `Cache-Control: no-cache, no-store, max-age=0, must-revalidate`.

Resolve the product from the new campaign request or known hierarchy context. An
omitted, `UNSET`, or `UNKNOWN` campaign product maps to AUCTION. Campaign responses do
not consistently expose `ad_product`, so do not silently classify an existing hierarchy
as AUCTION when its configuration or request context indicates CONTENT or FPMNG. See
`ad-product-validation.md` for the complete resolution and interaction procedure.

**Response:** 200 — Ad product catalog with validation rules per product type.

**Response structure:**

Each product has `campaign`, `ad_set`, and `ad` sections. Rules are separated by
operation: use `create` plus `both` for POST requests and `update` plus `both` for PATCH
requests. Some products omit an operation section when they have no additional rules
for it.

The following is an abbreviated structural example. Always use values from the live
response rather than treating this sample as an exhaustive catalog.

```json
{
"description": "Ad product validation rules that layer on top of the OpenAPI spec...",
"ad_products": {
"AUCTION": {
"display_name": "...",
"description": "...",
"campaign": {
"create": {
"allowed_values": { "objective": ["<live product-specific values>"] }
},
"update": {
"allowed_values": { "status": ["ACTIVE", "PAUSED"] },
"restrictions": ["field: cannot change after creation"]
},
"both": {
"constraints": ["end_time must be within 365 days of start_time"],
"cross_field_rules": ["When X: Y"]
}
},
"ad_set": {
"create": {
"allowed_values": { "...": "..." },
"required_fields": ["..."],
"forbidden_fields": ["..."],
"cross_field_rules": ["..."]
},
"update": {
"allowed_values": { "...": "..." },
"restrictions": ["..."],
"cross_field_rules": ["..."]
},
"both": {
"frequency_caps": { "max_impressions": { "DAY": 5, "WEEK": 35, "MONTH": 50 } },
"constraints": ["..."],
"cross_field_rules": ["..."]
}
},
"ad": {
"create": {
"required_fields": ["assets.asset_id: primary asset required; must not be archived"]
},
"update": { "restrictions": ["Cannot edit an archived creative"] },
"both": {
"constraints": ["Audio creative duration: max 31,000 ms"]
}
}
}
}
}
```

**How to apply rules:**
- For a **POST**: apply every rule category present in `create` and `both`.
- For a **PATCH**: fetch the current entity, deep-merge the patch, then apply every rule category present in `update` and `both` to the effective entity.
- Match the entity type to the correct key: `campaign`, `ad_set`, or `ad`.
- Treat `required_fields`, `forbidden_fields`, `constraints`, `restrictions`, and
`cross_field_rules` as semantic rules, not merely field-name arrays.
- Retrieve parent entities, assets, estimates, prices, or reporting state when a rule
depends on them.
- Do not report runtime or server-only conditions as passed unless the required state
was actually checked.

---

## Estimates

**Important:** These are top-level endpoints — they are NOT nested under `/ad_accounts/{ad_account_id}/`. The `ad_account_id` is passed in the request body instead.
Expand Down
Loading