Apple Search Ads
Install
See the Install guide for the full setup, including Windows PowerShell.
curl -fsSL https://install.skippr.io/install.sh | shClick to copyInstalling Skippr means accepting the Skippr EULA.
Extracts curated daily report tables from the Apple Search Ads Campaign Management API v5. Each namespace is one bronze grain with replace-by-date landing semantics (replace_partition on date).
How it works
- Authenticates with an ES256-signed OAuth client secret (or a static access token for local debugging).
- For each selected stream, requests one calendar day per API call with
granularity: DAILY. - Campaign, ad group, and search term reports fan out per campaign; keyword reports fan out per campaign × ad group (bounded by
max_concurrent_requests). - Search term reports always send
timeZone: ORTZ(Apple requirement); other streams usetime_zone(defaultUTC). - Skips the trailing
processing_lag_days(default 1). - Re-syncs
lookback_days(default 3) of mature dates;replace_partitionondateoverwrites revised spend/install metrics.
Discover sampling (automatic)
skippr discover stays fast even when stream_profile: full is configured:
- Samples the last 3 calendar days of
apple_search_ads.campaign_dailyonly - Does not load checkpoints, apply lookback, or enumerate campaigns/ad groups
- Namespace contracts still reflect your configured profile (e.g. all four namespaces on
full)
Full historical sync and all grains run on skippr sync.
Configuration
Engine skippr.yml (runtime plugin):
pipelines:
picnic_asa:
data_source: data_sources.asa
data_sink: data_sinks.athena
data_sources:
asa:
AppleSearchAds:
org_id: "${APPLE_SEARCH_ADS_ORG_ID}"
client_id: "${APPLE_SEARCH_ADS_CLIENT_ID}"
team_id: "${APPLE_SEARCH_ADS_TEAM_ID}"
key_id: "${APPLE_SEARCH_ADS_KEY_ID}"
private_key_path: "${APPLE_SEARCH_ADS_PRIVATE_KEY_PATH}"
start_date: "2024-01-01"
stream_profile: full
lookback_days: 3
max_concurrent_requests: 8Public skippr.yaml (via skippr connect):
source:
kind: apple_search_ads
org_id: "${APPLE_SEARCH_ADS_ORG_ID}"
client_id: "${APPLE_SEARCH_ADS_CLIENT_ID}"
team_id: "${APPLE_SEARCH_ADS_TEAM_ID}"
key_id: "${APPLE_SEARCH_ADS_KEY_ID}"
private_key_path: "${APPLE_SEARCH_ADS_PRIVATE_KEY_PATH}"
start_date: "2024-01-01"
stream_profile: full| Field | Default | Description |
|---|---|---|
org_id | (required) | Apple Search Ads organization ID (X-AP-Context: orgId=…) |
client_id | (required) | OAuth client ID from Apple |
team_id | (required) | Apple team ID (JWT iss; subject is SEARCHADS.{team_id}) |
key_id | (required) | API key ID (kid on the ES256 JWT) |
private_key_path | Path to EC private key PEM (.p8 / PEM) | |
private_key_pem | Inline PEM (alternative to private_key_path) | |
start_date | (required) | First date to sync (YYYY-MM-DD) |
end_date | Last date; omit to sync through yesterday minus lag | |
stream_profile | full | minimal (1), standard (2), or full (4 namespaces) |
streams | profile set | Comma-separated namespaces; overrides profile when set |
lookback_days | 3 | Mature days to re-fetch each run |
processing_lag_days | 1 | Skip syncing the last N calendar days |
time_zone | UTC | Report timeZone (ignored for search_term_daily — uses ORTZ) |
return_records_with_no_metrics | true | Include rows with zero metrics |
max_concurrent_requests | 8 | Parallel cap for per-campaign / per-ad-group report calls |
access_token | Bearer token override (${APPLE_SEARCH_ADS_ACCESS_TOKEN}) |
Stream profiles
| Profile | Namespaces | Use case |
|---|---|---|
full | 4 | Production default (all daily grains) |
standard | 2 | Campaign + ad group only (lighter fan-out) |
minimal | 1 | Discover / CI (campaign_daily only) |
Bronze catalog (stream_profile: full)
| Namespace | Grain | Fan-out | timeZone |
|---|---|---|---|
apple_search_ads.campaign_daily | Campaign | Org-wide | UTC (or time_zone) |
apple_search_ads.ad_group_daily | Ad group | Per campaign | UTC |
apple_search_ads.keyword_daily | Keyword | Per campaign × ad group | UTC |
apple_search_ads.search_term_daily | Search term | Per campaign | ORTZ (fixed) |
Rows include org_id, date, grain IDs (campaign_id, ad_group_id, keyword_id, search_term), and Apple metric fields (e.g. localSpend, impressions, taps, install variants) without renaming nested money objects.
Landing semantics
Daily Search Ads reports are mutable. This source uses replace_partition on date with lookback_days as the refresh window.
CLI
skippr connect source apple-search-ads \
--org-id "${APPLE_SEARCH_ADS_ORG_ID}" \
--client-id "${APPLE_SEARCH_ADS_CLIENT_ID}" \
--team-id "${APPLE_SEARCH_ADS_TEAM_ID}" \
--key-id "${APPLE_SEARCH_ADS_KEY_ID}" \
--private-key-path "${APPLE_SEARCH_ADS_PRIVATE_KEY_PATH}" \
--start-date 2024-01-01 \
--stream-profile full \
--lookback-days 3 \
--max-concurrent-requests 8Authentication
Skippr exchanges an ES256 JWT (signed with your Apple API private key) for a short-lived access token at https://appleid.apple.com/auth/oauth2/token (grant_type=client_credentials, scope=searchadsorg).
Required env vars (typical):
APPLE_SEARCH_ADS_ORG_IDAPPLE_SEARCH_ADS_CLIENT_IDAPPLE_SEARCH_ADS_TEAM_IDAPPLE_SEARCH_ADS_KEY_IDAPPLE_SEARCH_ADS_PRIVATE_KEY_PATH
Optional: APPLE_SEARCH_ADS_ACCESS_TOKEN to skip JWT exchange in local debugging.
Recommended destination
Athena (S3 + Glue) with pipeline transform:
pipelines:
picnic_asa:
transform:
batch_time_fields: [date]Bronze paths look like bronze/apple_search_ads.campaign_daily/date=2024-01-15/. Run skippr discover after the first sync.
Troubleshooting
| Symptom | Fix |
|---|---|
| Token / 401 errors | Verify client_id, team_id, key_id, and PEM path; regenerate key in Apple Search Ads UI |
| Empty keyword stream | Confirm campaigns and ad groups exist; check max_concurrent_requests and API rate limits |
| Search term errors | Ensure the plugin sends ORTZ (built-in for search_term_daily) |
| Slow discover | Expected — discover only samples 3 days of campaign_daily; use skippr sync for full history |
| Stale metrics | Confirm replace_partition; increase lookback_days |
Offline dev: set SKIPPR_APPLE_SEARCH_ADS_FIXTURE_DIR to JSON fixtures (campaign_report.json, ad_group_report.json, etc.).
