asc-apple-ads
Use when managing Apple Ads with asc, including OAuth profiles, ad-account discovery, Platform API v1 campaigns and targeting, reports, assets, recommendations, guarded mutations, raw requests, and Campaign Management API v5 migration.
Works with
---
name: asc-apple-ads
description: Use when managing Apple Ads with asc, including OAuth profiles, ad-account discovery, Platform API v1 campaigns and targeting, reports, assets, recommendations, guarded mutations, raw requests, and Campaign Management API v5 migration.
license: MIT
---
# asc Apple Ads
Run Apple Ads work through `asc ads`. Apple Ads credentials are separate from App Store Connect credentials; `asc auth login` does not configure Ads.
## Pick the API first
- Direct `asc ads <resource> ...` commands use Apple Ads Platform API v1 and an ad account ID.
- Deprecated Campaign Management API v5 commands live under `asc ads v5 ...` and use an organization ID. Apple retires v5 on January 26, 2027.
- Never substitute an org ID for an ad account ID. The CLI keeps them separate.
- Run the exact leaf command with `--help` before building a request file. Platform v1 payloads and response envelopes differ from v5; the CLI does not translate them.
- For non-interactive pipelines, pass `--file -` to read a JSON request body from stdin; the CLI rejects it when stdin is a terminal.
- Resource, report, upload, and raw commands emit lossless JSON. Use `jq` for projections instead of asking for table or markdown output.
## Authenticate and pin the account
Store both contexts when a profile must support v1 and legacy v5:
```bash
asc ads auth login \
--name "Marketing" \
--client-id "$ASC_ADS_CLIENT_ID" \
--team-id "$ASC_ADS_TEAM_ID" \
--key-id "$ASC_ADS_KEY_ID" \
--private-key "$ASC_ADS_PRIVATE_KEY_PATH" \
--ad-account "987654" \
--org "123456" \
--network
```
For CI, set Ads-specific variables and bypass the host keychain:
```bash
export ASC_ADS_CLIENT_ID="SEARCHADS_CLIENT_ID"
export ASC_ADS_TEAM_ID="SEARCHADS_TEAM_ID"
export ASC_ADS_KEY_ID="KEY_ID"
export ASC_ADS_PRIVATE_KEY_PATH="$HOME/.asc/apple-ads-private-key.pem"
export ASC_ADS_AD_ACCOUNT_ID="987654"
export ASC_ADS_BYPASS_KEYCHAIN=1
```
`ASC_ADS_PRIVATE_KEY` and `ASC_ADS_PRIVATE_KEY_B64` also work. If another trusted process minted a short-lived token, set `ASC_ADS_ACCESS_TOKEN`; scoped v1 calls still need an ad account ID.
Check auth without printing a token:
```bash
asc ads auth status --validate --output json
asc ads auth discover --ads-profile "Marketing" --output json
asc ads auth doctor --output json
```
Discovery calls Platform v1 `GET /v1/me` and `GET /v1/acls`. Compare each ACL's ad-account ID, name, organization ID, and roles; never select the first result automatically. Print the chosen account before any mutation, then pass both `--ads-profile "Marketing"` and `--ad-account "987654"` when more than one profile or account is available.
For named profiles, the profile's `ad_account_id` and `org_id` stand alone; they do not inherit context from another profile or root config. V1 context precedence is `--ad-account`, `ASC_ADS_AD_ACCOUNT_ID`, the selected profile, then profile-less root config. Legacy v5 uses the matching `--org` and `ASC_ADS_ORG_ID` chain.
## Start read-only
Identity and ACL calls need no ad account context:
```bash
asc ads me view --ads-profile "Marketing" --output json
asc ads acls list --ads-profile "Marketing" --output json
asc ads orgs view --ads-profile "Marketing" --org-id "123456" --output json
```
Then prove the selected account with a small app search:
```bash
asc ads apps search \
--ads-profile "Marketing" \
--ad-account "987654" \
--query "Example" \
--limit 1 \
--output json
```
App search requires at least one of `--query`, `--cpids`, or `--return-owned-apps`. Storefronts use comma-separated ISO alpha-2 codes. Add `--paginate` only when every search result is needed.
Use each resource's `find` command for inventory. Most v1 queries put filters, sorting, and pagination in a JSON object. A subordinate-resource filter looks like this:
```json
{
"filters": [
{"field": "campaignId", "operator": "EQUALS", "value": ["campaign-id"]}
],
"pagination": {"offset": 0, "pageSize": 100, "fetchTotalCount": true}
}
```
```bash
asc ads campaigns find --ads-profile "Marketing" --ad-account "987654" --output json
asc ads ad-groups find --ads-profile "Marketing" --ad-account "987654" --file query.json --output json
asc ads ads find --ads-profile "Marketing" --ad-account "987654" --file query.json --output json
```
Omitting `--file` from `campaigns find` requests the default first page. To control or exhaust the result set, use `pagination.offset`, `pageSize`, and `fetchTotalCount` in a query file, read the response pagination, and advance the offset until complete. This command has no `--paginate` flag. Platform filters use the singular `value`; do not copy v5 `Selector` fields such as `conditions` or plural `values`, which current `asc` rejects before auth.
The direct v1 tree also covers ad accounts and advertiser resources; app eligibility, locales, product pages, and rejection reasons; brands, business categories, locations, location groups, creatives, and assets; geographic targeting and shared budgets; reports for apps and brands; insights, suggestions, recommendations, and change history. Discover the exact leaf instead of falling back to raw HTTP:
```bash
asc ads change-history --help
asc ads suggestions --help
asc ads rejection-reasons --help
asc ads reports brands --help
```
Keyword queries need a selector file. Targeting keywords require an `id`, `adGroupId`, or `campaignId` filter. Negative keywords require `id` or `adGroupId`; campaign-level negative keywords combine `campaignId` with an `adGroupId` filter whose operator is `IS_NULL`.
```bash
asc ads targeting-keywords find --ads-profile "Marketing" --ad-account "987654" --file keyword-query.json --output json
asc ads negative-keywords find --ads-profile "Marketing" --ad-account "987654" --file negative-keyword-query.json --output json
```
## Reports and optimization
V1 reports require an endpoint-specific body. Dates live under `timeRange`, page controls use `offset` and `pageSize`, and campaign or ad-group IDs belong in `filters`:
```json
{
"pagination": {"offset": 0, "pageSize": 20},
"filters": [
{"field": "campaignId", "operator": "EQUALS", "value": ["campaign-id"]}
],
"groupBy": ["countryOrRegion"],
"timeRange": {
"start": "2026-08-01",
"end": "2026-08-14",
"timeZone": "ORTZ",
"granularity": "DAILY"
}
}
```
```bash
asc ads reports apps campaigns \
--ads-profile "Marketing" \
--ad-account "987654" \
--file report.json \
--output json
```
Report commands do not accept `--paginate`; change pagination in the body. Inspect the leaf help because report entities accept different `groupBy` and option values.
Recommendations and suggestions also use endpoint-specific bodies. Applying or dismissing recommendations can change spend and requires `--confirm`:
```bash
asc ads recommendations daily-budgets find --ads-profile "Marketing" --ad-account "987654" --file query.json
asc ads recommendations daily-budgets apply --ads-profile "Marketing" --ad-account "987654" --file recommendations.json --confirm
```
## Guard mutations
Do not mutate until the user has approved the ad account, resource type, target IDs, and reviewed payload. Keep request JSON in files; never invent fields from a related v5 schema.
Campaign creation may start spending. `CampaignCreate` requires `adAccountId`, `billingEvent`, `dailyBudget`, `name`, `promotedObjectId`, `promotedObjectType`, and `targeting`. The CLI sends the file unchanged: `--ad-account` selects the request context but does not inject `adAccountId` into the JSON. Start from this paused shape, replace every placeholder with values read from the selected account, and recheck the current Apple v1 schema for any account-specific requirements:
```json
{
"name": "ASC agent validation 2026-08-15T00:00:00Z",
"status": "PAUSED",
"adAccountId": 987654,
"promotedObjectType": "APPSTORE_APP",
"promotedObjectId": "123456789",
"billingEvent": "TAPS",
"dailyBudget": {"value": {"amount": "1", "currency": "USD"}},
"startTime": "2030-01-01T00:00:00.000",
"endTime": "2030-01-02T00:00:00.000",
"targeting": {"countryOrRegion": {"include": ["US"]}},
"bidStrategy": {"bidStrategyType": "MANUAL_CPT", "bidStrategyGoal": "TAP"}
}
```
A payload with explicit top-level `"status":"PAUSED"` can run without `--confirm`; an omitted or non-paused status requires it.
```bash
asc ads campaigns create --ads-profile "Marketing" --ad-account "987654" --file paused-campaign.json
asc ads campaigns pause --ads-profile "Marketing" --ad-account "987654" --campaign "campaign-id"
asc ads campaigns resume --ads-profile "Marketing" --ad-account "987654" --campaign "campaign-id" --confirm
```
Campaign updates need `--confirm` when they can change budget, targeting, bids, delivery, dates, or status. A name-only update or a name plus `PAUSED` status does not. Deletes, bulk creates or updates, recommendation apply or dismiss calls, budget-order writes, and other operationally risky mutations require confirmation before auth or network access.
Ad-group creation and keyword bulk writes are examples of always-confirmed delivery or targeting changes. V1 bulk keyword files use wrapper objects such as `KeywordCreateBulkRequest`, not the v5 raw-array shape:
```bash
asc ads ad-groups create --ads-profile "Marketing" --ad-account "987654" --file ad-group.json --confirm
asc ads targeting-keywords create-bulk --ads-profile "Marketing" --ad-account "987654" --file keywords.json --confirm
asc ads targeting-keywords delete --ads-profile "Marketing" --ad-account "987654" --keyword "keyword-id" --confirm
```
Shared budgets use the `budget-orders` command group. Create, update, and delete are context-free but require confirmation; view and find accept optional ad-account context.
```bash
asc ads budget-orders create --ads-profile "Marketing" --file shared-budget.json --confirm
asc ads budget-orders update --ads-profile "Marketing" --budget-order "budget-id" --file update.json --confirm
asc ads budget-orders delete --ads-profile "Marketing" --budget-order "budget-id" --confirm
```
Ad-account creation also requires `--confirm` because its account family cannot change and Apple provides no delete endpoint. An ad-account update containing `delegations` requires confirmation because it replaces the complete list.
Use the dedicated multipart command for brand assets. Poll until Apple finishes processing:
```bash
asc ads assets upload --ads-profile "Marketing" --file ./brand.png --brand "BRAND_ID" --ad-account "987654"
asc ads assets view --ads-profile "Marketing" --asset "ASSET_UUID" --ad-account "987654"
```
Only use an asset when `eligibility.status` is `ELIGIBLE`; for `LIMITED`, inspect `allowedGroups`. Do not attach `PENDING` or `INELIGIBLE` assets.
## Raw requests
Use first-class commands for routine work. Raw v1 requests accept only `v1/...` paths or `https://api.ads.apple.com/v1/...` URLs:
```bash
asc ads api request \
--method POST \
--path v1/campaigns/query \
--ads-profile "Marketing" \
--ad-account "987654" \
--file query.json \
--output json
```
Unknown mutations fail closed, and risky known mutations require `--confirm`. The raw command rejects multipart asset upload; use `asc ads assets upload`.
Keep legacy calls explicit:
```bash
asc ads v5 api request \
--method POST \
--path v5/campaigns/find \
--ads-profile "Marketing" \
--org "123456" \
--file selector.json \
--output json
```
## Migrate v5 one command at a time
Keep existing v5 payloads under `asc ads v5` until each script has a reviewed v1 body and response parser. Common moves:
| Deprecated v5 | Platform API v1 |
| --- | --- |
| `asc ads v5 campaigns list` | `asc ads campaigns find` |
| `asc ads v5 apps localized-details` | `asc ads apps locales find` |
| `asc ads v5 product-pages list` | `asc ads product-pages find` |
| `asc ads v5 reports campaigns` | `asc ads reports apps campaigns` |
| `asc ads v5 campaigns pause` / `resume` | `asc ads campaigns pause` / `resume` |
| v5 campaign or ad-group negative keywords | `asc ads negative-keywords ...` with scope in the body |
Seven v5 leaves have no one-command v1 replacement: product-page countries, product-page devices, targeting-keyword bulk delete, both negative-keyword bulk deletes, and impression-share report list and view. Do not pretend that `geo search`, `insights impression-share`, or single-resource deletes preserve those contracts.
## Finish live tests cleanly
- Start with ACL discovery and a one-result app search.
- Use a unique timestamped name and explicit `PAUSED` status for disposable campaign tests.
- Save every created ID from JSON output.
- Pause spend-bearing resources before checking anything else.
- Reread a test campaign with `asc ads campaigns view --ads-profile "Marketing" --ad-account "987654" --campaign "campaign-id" --output json`.
- Delete only test-created campaigns with `asc ads campaigns delete --ads-profile "Marketing" --ad-account "987654" --campaign "campaign-id" --confirm`; do not delete a pre-existing parent.
- Run the same `campaigns view` again. Treat Apple's not-found response as cleanup proof; report any campaign that still exists or could not be removed.More General & Other skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
grill-me
mattpocock/skills
A relentless interview to sharpen a plan or design.
grill-with-docs
mattpocock/skills
A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.

