Install
openclaw skills install apple-search-adsClawHub Security found sensitive or high-impact capabilities. Review the scan results before using.
Create, optimize, and scale Apple Search Ads campaigns with API automation, attribution integration, and bid strategy recommendations.
openclaw skills install apple-search-adsComplete toolkit for Apple Search Ads: Campaign Management API v5, attribution integration (AdServices + SKAdNetwork), bid optimization, and strategic recommendations.
On first use, read setup.md for integration guidelines.
User needs to run Apple Search Ads for iOS apps. Agent handles campaign creation, bid optimization, attribution tracking, performance analysis, and strategic recommendations.
Memory lives in ~/apple-search-ads/. See memory-template.md for structure.
~/apple-search-ads/
├── memory.md # Active campaigns, preferences, learnings
├── credentials.md # OAuth config (NEVER commit real secrets)
├── campaigns/ # Campaign-specific notes and performance
│ └── {app-id}/
├── reports/ # Generated reports
└── scripts/ # Custom automation
| Topic | File |
|---|---|
| Setup process | setup.md |
| Memory template | memory-template.md |
| API endpoints | api-reference.md |
| iOS integration | ios-integration.md |
| Strategy guide | strategy.md |
| Script library | scripts.md |
Apple Ads API uses OAuth with client credentials. Generate credentials at: https://app.searchads.apple.com/cm/app/settings/apicertificates
# 1. Generate client secret (JWT signed with private key)
# Header
{
"alg": "ES256",
"kid": "{KEY_ID}"
}
# Payload
{
"sub": "{CLIENT_ID}",
"aud": "https://appleid.apple.com",
"iat": {CURRENT_TIMESTAMP},
"exp": {TIMESTAMP_+180_DAYS},
"iss": "{TEAM_ID}"
}
# 2. Exchange for access token
curl -X POST "https://appleid.apple.com/auth/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id={CLIENT_ID}" \
-d "client_secret={CLIENT_SECRET}" \
-d "scope=searchadsorg"
# Response contains access_token (valid 1 hour)
Base URL: https://api.searchads.apple.com/api/v5
Headers:
Authorization: Bearer {ACCESS_TOKEN}
X-AP-Context: orgId={ORG_ID}
Content-Type: application/json
| Resource | Method | Endpoint |
|---|---|---|
| Apps | ||
| Search apps | POST | /search/apps |
| App eligibility | GET | /apps/{adamId}/eligibilities |
| Campaigns | ||
| List campaigns | GET | /campaigns |
| Create campaign | POST | /campaigns |
| Update campaign | PUT | /campaigns/{id} |
| Delete campaign | DELETE | /campaigns/{id} |
| Ad Groups | ||
| List ad groups | GET | /campaigns/{id}/adgroups |
| Create ad group | POST | /campaigns/{id}/adgroups |
| Keywords | ||
| List keywords | GET | /campaigns/{cId}/adgroups/{agId}/targetingkeywords |
| Add keywords | POST | /campaigns/{cId}/adgroups/{agId}/targetingkeywords/bulk |
| Reports | ||
| Campaign report | POST | /reports/campaigns |
| Ad group report | POST | /reports/campaigns/{id}/adgroups |
| Keyword report | POST | /reports/campaigns/{cId}/adgroups/{agId}/keywords |
| Search term report | POST | /reports/campaigns/{cId}/searchterms |
| Impression share | POST | /reports/campaigns/{id}/impressionshare |
Organization (orgId)
└── Campaign (Search Results / Search Tab / Today Tab)
├── Budget & Schedule
├── Countries/Regions
└── Ad Groups
├── Keywords (targeting + negative)
├── Audience (age, gender, device, etc.)
├── Creatives (default or Custom Product Pages)
└── Bid settings
| Type | Placement | Best For |
|---|---|---|
| Search Results | Top of search results | High-intent users, brand defense |
| Search Tab | Suggested apps before search | Discovery, broad reach |
| Today Tab | Today tab featured | Brand awareness, launches |
{
"name": "MyApp - US - Brand",
"adamId": 123456789,
"countriesOrRegions": ["US"],
"budgetAmount": {"amount": "1000", "currency": "USD"},
"dailyBudgetAmount": {"amount": "50", "currency": "USD"},
"supplySources": ["APPSTORE_SEARCH_RESULTS"],
"billingEvent": "TAPS",
"status": "ENABLED",
"startTime": "2026-01-01T00:00:00.000",
"endTime": null
}
{
"name": "Brand Keywords",
"campaignId": 123456,
"defaultBidAmount": {"amount": "1.50", "currency": "USD"},
"cpaGoal": {"amount": "5.00", "currency": "USD"},
"startTime": "2026-01-01T00:00:00.000",
"targetingDimensions": {
"age": {"included": [{"minAge": 18}]},
"gender": {"included": ["M", "F"]},
"deviceClass": {"included": ["IPHONE", "IPAD"]},
"daypart": {"userTime": {"included": [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23]}},
"adminArea": null,
"locality": null,
"appDownloaders": {"included": [], "excluded": []}
},
"automatedKeywordsOptIn": false,
"status": "ENABLED"
}
| Type | Behavior | Use Case |
|---|---|---|
| Exact | Query = keyword exactly | Brand terms, proven converters |
| Broad | Synonyms, related terms | Discovery, expansion |
| Search Match | Auto-matched by Apple | New apps, keyword research |
{
"text": "meditation app",
"matchType": "EXACT",
"bidAmount": {"amount": "2.00", "currency": "USD"},
"status": "ACTIVE"
}
Week 1: Set baseline bids (industry avg or $1-2)
↓
Week 2: Review search term report
- High converts, low bid → raise bid 20-30%
- Low converts, high spend → lower bid or pause
- Irrelevant terms → add as negative
↓
Week 3+: Repeat. Target CPA within 20% of goal.
Modern attribution without user tracking. Integrates directly in iOS app.
import AdServices
func trackAttribution() async {
do {
// 1. Get attribution token from device
let token = try AAAttribution.attributionToken()
// 2. Send to Apple's attribution API
var request = URLRequest(url: URL(string: "https://api-adservices.apple.com/api/v1/")!)
request.httpMethod = "POST"
request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
request.httpBody = token.data(using: .utf8)
let (data, _) = try await URLSession.shared.data(for: request)
let attribution = try JSONDecoder().decode(Attribution.self, from: data)
// 3. attribution contains: campaignId, adGroupId, keywordId, etc.
// Send to your analytics backend
} catch {
// Not from Apple Search Ads or error
}
}
struct Attribution: Codable {
let attribution: Bool
let orgId: Int?
let campaignId: Int?
let adGroupId: Int?
let keywordId: Int?
let creativeSetId: Int?
let conversionType: String?
let clickDate: String?
}
Privacy-focused attribution for installs. Apple aggregates data, no user-level tracking.
// In your app delegate
import StoreKit
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Register for attribution
SKAdNetwork.updatePostbackConversionValue(0) { error in
// Initial registration
}
return true
}
// When user completes valuable action (purchase, signup, etc.)
func trackConversion(value: Int) {
// value 0-63, represents conversion value
SKAdNetwork.updatePostbackConversionValue(value) { error in
if error == nil {
// Updated successfully
}
}
}
| Value | Meaning | Example |
|---|---|---|
| 0 | Install only | App opened |
| 1-10 | Engagement tier | Sessions, time in app |
| 11-30 | Feature usage | Key feature activated |
| 31-50 | Monetization signal | Trial started, content viewed |
| 51-63 | Revenue tier | Purchase completed |
If using an MMP, they handle AdServices and SKAdNetwork. Follow their SDK docs. Key integration points:
{
"startTime": "2026-01-01",
"endTime": "2026-01-31",
"timeZone": "UTC",
"granularity": "DAILY",
"selector": {
"orderBy": [{"field": "localSpend", "sortOrder": "DESCENDING"}],
"pagination": {"offset": 0, "limit": 100}
},
"returnRowTotals": true,
"returnGrandTotals": true
}
| Metric | Description | Good Range |
|---|---|---|
| TTR | Tap-through rate | 5-10%+ |
| CVR | Conversion rate (installs/taps) | 30-60% |
| CPA | Cost per acquisition | < LTV/3 |
| CPT | Cost per tap | $0.50-3.00 (varies) |
| ROAS | Return on ad spend | > 100% |
| Impression Share | % of eligible impressions won | Track trend |
Critical for optimization. Shows actual queries that triggered your ads.
// POST /reports/campaigns/{campaignId}/searchterms
{
"startTime": "2026-01-01",
"endTime": "2026-01-31",
"selector": {
"conditions": [
{"field": "impressions", "operator": "GREATER_THAN", "values": ["10"]}
],
"orderBy": [{"field": "impressions", "sortOrder": "DESCENDING"}],
"pagination": {"offset": 0, "limit": 1000}
}
}
Weekly ritual:
Campaign: [App] - [Country] - Brand
└── Ad Group: Brand Exact
└── Keywords: app name, brand terms (exact match)
Campaign: [App] - [Country] - Category
└── Ad Group: Category - Exact
└── Keywords: category terms (exact match)
└── Ad Group: Category - Discovery
└── Search Match enabled, low bid
Campaign: [App] - [Country] - Competitor
└── Ad Group: Competitor Names
└── Keywords: competitor app names (exact match)
| Stage | Brand | Category | Competitor | Discovery |
|---|---|---|---|---|
| Launch | 40% | 40% | 10% | 10% |
| Growth | 20% | 50% | 20% | 10% |
| Scale | 10% | 60% | 25% | 5% |
Localization checklist:
Create variations of your App Store page for different audiences.
// Get available CPPs
// GET /apps/{adamId}/customproductpages
// Create ad using CPP
{
"name": "Fitness Ad - Summer Campaign",
"adGroupId": 12345,
"creativeType": "CUSTOM_PRODUCT_PAGE",
"productPageId": "cpp-uuid-here"
}
Best practices:
See scripts.md for complete script library. Key scripts:
#!/usr/bin/env bash
set -euo pipefail
# SECURITY MANIFEST:
# Environment variables accessed: ASA_CLIENT_ID, ASA_TEAM_ID, ASA_KEY_ID, ASA_PRIVATE_KEY
# External endpoints called: https://appleid.apple.com/auth/oauth2/token (only)
# Local files read: none
# Local files written: none
# Requires: openssl, jq
CLIENT_ID="${ASA_CLIENT_ID}"
TEAM_ID="${ASA_TEAM_ID}"
KEY_ID="${ASA_KEY_ID}"
PRIVATE_KEY="${ASA_PRIVATE_KEY}" # PEM format
# Create JWT header
HEADER=$(echo -n '{"alg":"ES256","kid":"'$KEY_ID'"}' | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
# Create JWT payload
NOW=$(date +%s)
EXP=$((NOW + 15552000)) # 180 days
PAYLOAD=$(echo -n '{"sub":"'$CLIENT_ID'","aud":"https://appleid.apple.com","iat":'$NOW',"exp":'$EXP',"iss":"'$TEAM_ID'"}' | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
# Sign with private key
SIGNATURE=$(echo -n "$HEADER.$PAYLOAD" | openssl dgst -sha256 -sign <(echo "$PRIVATE_KEY") | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
CLIENT_SECRET="$HEADER.$PAYLOAD.$SIGNATURE"
# Exchange for access token
curl -s -X POST "https://appleid.apple.com/auth/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&scope=searchadsorg" \
| jq -r '.access_token'
#!/usr/bin/env bash
set -euo pipefail
# SECURITY MANIFEST:
# Environment variables accessed: ASA_ACCESS_TOKEN, ASA_ORG_ID
# External endpoints called: https://api.searchads.apple.com/api/v5/reports/campaigns (only)
# Local files read: none
# Local files written: stdout (report data)
ACCESS_TOKEN="${ASA_ACCESS_TOKEN}"
ORG_ID="${ASA_ORG_ID}"
TODAY=$(date -u +%Y-%m-%d)
YESTERDAY=$(date -u -v-1d +%Y-%m-%d 2>/dev/null || date -u -d "yesterday" +%Y-%m-%d)
curl -s -X POST "https://api.searchads.apple.com/api/v5/reports/campaigns" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "X-AP-Context: orgId=$ORG_ID" \
-H "Content-Type: application/json" \
-d '{
"startTime": "'$YESTERDAY'",
"endTime": "'$TODAY'",
"granularity": "DAILY",
"selector": {
"orderBy": [{"field": "localSpend", "sortOrder": "DESCENDING"}]
},
"returnRowTotals": true,
"returnGrandTotals": true
}' | jq '.data.reportingDataResponse.row[] | {
campaign: .metadata.campaignName,
spend: .total.localSpend.amount,
impressions: .total.impressions,
taps: .total.taps,
installs: .total.installs,
cpa: (if .total.installs > 0 then (.total.localSpend.amount | tonumber) / .total.installs else "N/A" end)
}'
Brand, category, competitor, and discovery keywords in separate campaigns. Mixing makes optimization impossible.
Begin with exact match keywords you're confident about. Use Search Match and broad only for discovery with low bids.
The search term report is gold. Review weekly, add winners as exact, add losers as negatives.
Competitors WILL bid on your brand name. Always have a brand campaign with high bids to protect your real estate.
Implement AdServices for iOS 14.3+. Without attribution, you're optimizing blind.
Mixing countries makes bid optimization nearly impossible. Create separate campaigns per country/region.
Set CPA goals, not just budgets. If CPA is 2x target, pause and investigate before spending more.
| Endpoint | Data Sent | Purpose |
|---|---|---|
https://appleid.apple.com/auth/oauth2/token | Client credentials (JWT) | Get access token |
https://api.searchads.apple.com/api/v5/* | Campaign/keyword data | Campaign management |
https://api-adservices.apple.com/api/v1/ | Attribution token | Attribution data |
No other data is sent externally.
Data that leaves your machine:
Data that stays local:
~/apple-search-ads/credentials.mdThis skill does NOT:
By using this skill, data is sent to Apple's Search Ads API and AdServices. Only install if you trust Apple with your advertising data.
Install with clawhub install <slug> if user confirms:
app-store-connect — manage apps and releasesaso — App Store Optimizationanalytics — track metrics and KPIsios — iOS development patternsclawhub star apple-search-adsclawhub sync