verification
use it
# add to stunt.yaml
services:
apple-searchads:
adapter: embedded:apple-searchads-style
# then
$ stunt upThe embedded: source extracts this adapter from the binary — no git clone, fully offline.
how to mock the Apple Search Ads API locally
Install stunt, paste the service block above into stunt.yaml, run stunt up — then point your client at localhost instead of Apple Search Ads API. It works offline, keeps state across requests and restarts, and every response is synthetic: the route shapes, status codes, pagination, and webhook delivery follow the real Apple Search Ads API v4 surface — the data is fake, the behavior is real.
backing stores
campaignsadskeywords
api surface (12)
generated from the adapter manifest — exactly what boots, nothing more
12 routes
POST/api/oauth2/token
POST/api/v4/campaigns/find
POST/api/v4/campaigns
POST/api/v4/reports/campaigns
GET/api/v4/campaigns/{campaign_id}
PUT/api/v4/campaigns/{campaign_id}
POST/api/v4/campaigns/{campaign_id}/ads
POST/api/v4/campaigns/{campaign_id}/keywords/targeting/find
PUT/api/v4/campaigns/{campaign_id}/keywords/targeting/bulk
POST/api/v4/campaigns/{campaign_id}/keywords/targeting
PUT/api/v4/campaigns/{campaign_id}/keywords/targeting/{keyword_id}
DELETE/api/v4/campaigns/{campaign_id}/keywords/targeting/{keyword_id}
VM-verified behaviors (23)
- ✓a missing, malformed or unknown bearer is a 401 in the Search Ads envelope
- ✓the seeded static test token passes the gate
- ✓a client_credentials exchange mints a bearer valid for exactly one hour
- ✓malformed grants and client secrets are oauth-shaped 400s
- ✓campaign create assigns a numeric id and fills the v4 defaults
- ✓create without a name is a 400
- ✓the campaign reads back by numeric id; unknown ids are 404s
- ✓update merges fields, maps status onto servingStatus, and bumps only modificationTime
- ✓an invalid status is a 400 that writes nothing
- ✓ad groups create under a campaign; unknown campaigns are 404s
- ✓find lists the seed with default pagination and clock-derived stamps
- ✓conditions filter with EQUALS, CONTAINS and amount ranges
- ✓values within one condition are OR'd; multi-value NOT_EQUALS excludes all
- ✓orderBy sorts and pagination slices with totalResults before the slice
- ✓first touch seeds three keywords scoped to the campaign
- ✓keyword find speaks the same selector (conditions, orderBy, pagination)
- ✓create takes a single keyword or a batch body
- ✓single update merges partial fields and validates them
- ✓bulk update takes id-addressed rows; an unknown id 404s the batch
- ✓delete removes the keyword and find no longer sees it
- ✓startTime, endTime and groupBy are validated
- ✓one row per campaign with deterministic metrics and summed grandTotals
- ✓the report selector filters rows and grandTotals follow
Not implemented (4)
- No ad group find or update — only POST campaigns/{id}/ads create
- No negative keywords endpoints (keywords/negative find/create/update)
- No search-terms, keyword, or ads reports — only /reports/campaigns
- No audience targeting, geo/demographic dimensions, or bid adjustments
Differs from the real API (5)
- ES256 client-secret JWT is not signature-verified — structural checks only
- Reports group by campaign only; any other groupBy key returns 400
- Selector conditions cover a fixed field set per resource (id, name, budget, status, times)
- Bulk keyword update is non-atomic: rows before an unknown id persist though the batch 404s
- Unknown selector condition fields are silently ignored; real find/report 400s
want more of the Apple Search Ads API surface? adapters are YAML + Starlark — the authoring guide covers adding routes, fixtures, and state machines. Open an issue for anything big first, or just file a PR — merged routes ship in the next release.