Skip to content

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 copy

Installing 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

  1. Authenticates with an ES256-signed OAuth client secret (or a static access token for local debugging).
  2. For each selected stream, requests one calendar day per API call with granularity: DAILY.
  3. Campaign, ad group, and search term reports fan out per campaign; keyword reports fan out per campaign × ad group (bounded by max_concurrent_requests).
  4. Search term reports always send timeZone: ORTZ (Apple requirement); other streams use time_zone (default UTC).
  5. Skips the trailing processing_lag_days (default 1).
  6. Re-syncs lookback_days (default 3) of mature dates; replace_partition on date overwrites 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_daily only
  • 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):

yaml
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: 8

Public skippr.yaml (via skippr connect):

yaml
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
FieldDefaultDescription
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_pathPath to EC private key PEM (.p8 / PEM)
private_key_pemInline PEM (alternative to private_key_path)
start_date(required)First date to sync (YYYY-MM-DD)
end_dateLast date; omit to sync through yesterday minus lag
stream_profilefullminimal (1), standard (2), or full (4 namespaces)
streamsprofile setComma-separated namespaces; overrides profile when set
lookback_days3Mature days to re-fetch each run
processing_lag_days1Skip syncing the last N calendar days
time_zoneUTCReport timeZone (ignored for search_term_daily — uses ORTZ)
return_records_with_no_metricstrueInclude rows with zero metrics
max_concurrent_requests8Parallel cap for per-campaign / per-ad-group report calls
access_tokenBearer token override (${APPLE_SEARCH_ADS_ACCESS_TOKEN})

Stream profiles

ProfileNamespacesUse case
full4Production default (all daily grains)
standard2Campaign + ad group only (lighter fan-out)
minimal1Discover / CI (campaign_daily only)

Bronze catalog (stream_profile: full)

NamespaceGrainFan-outtimeZone
apple_search_ads.campaign_dailyCampaignOrg-wideUTC (or time_zone)
apple_search_ads.ad_group_dailyAd groupPer campaignUTC
apple_search_ads.keyword_dailyKeywordPer campaign × ad groupUTC
apple_search_ads.search_term_dailySearch termPer campaignORTZ (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.

See Source landing semantics.

CLI

bash
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 8

Authentication

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_ID
  • APPLE_SEARCH_ADS_CLIENT_ID
  • APPLE_SEARCH_ADS_TEAM_ID
  • APPLE_SEARCH_ADS_KEY_ID
  • APPLE_SEARCH_ADS_PRIVATE_KEY_PATH

Optional: APPLE_SEARCH_ADS_ACCESS_TOKEN to skip JWT exchange in local debugging.

Athena (S3 + Glue) with pipeline transform:

yaml
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

SymptomFix
Token / 401 errorsVerify client_id, team_id, key_id, and PEM path; regenerate key in Apple Search Ads UI
Empty keyword streamConfirm campaigns and ad groups exist; check max_concurrent_requests and API rate limits
Search term errorsEnsure the plugin sends ORTZ (built-in for search_term_daily)
Slow discoverExpected — discover only samples 3 days of campaign_daily; use skippr sync for full history
Stale metricsConfirm 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.).

Next steps