Guide

Apple Ads Campaign Management API v5 shuts down on January 26, 2027: what changes in the Platform API

Apple will turn off the Apple Ads Campaign Management API v5 (api.searchads.apple.com/api/v5) on January 26, 2027. After that date its endpoints return errors: no reports, no budget changes, no pauses. Anything that still calls v5 — a bidding script, a dashboard, a vendor's connector — stops on that day.

The replacement is the Apple Ads Platform API (api.ads.apple.com/v1). We moved our own connector across and ran both side by side on our own App Store account before switching anything. Below is what stayed the same, what changed, and the places where the live API did not match the documentation.

What does not change

  • Authentication. The same OAuth client-credentials flow, the same ES256 client secret and the same API user's key worked against the new API without re-issuing anything.
  • Campaign, ad group and keyword ids. Campaigns created under v5 kept their ids in the Platform API, so history keyed on those ids still lines up (on our account; check yours with a side-by-side read).
  • The numbers. Daily spend, impressions, taps and installs for the same days came back identical from both APIs. The metric names (localSpend, impressions, taps, totalInstalls, tapInstalls, viewInstalls) are the same.

What changes

The hierarchy gains a level — Org → Ad Account → Campaign — and almost every request and response shape moves with it.

v5Platform API
Base URLapi.searchads.apple.com/api/v5api.ads.apple.com/v1
Account context headerX-AP-Context: orgId=…X-AP-Context: adAccountId=…
Listing accountsGET /acls returns name, currency, time zoneGET /acls returns ids and roles only; currency and time zone come from GET /ad-accounts/{id}
Listing campaigns, ad groups, keywordsGET with limit and offset, totalResults in the responsePOST …/query with filters and pagination{offset, pageSize}; no total count. Deleted objects only appear if you ask for deleted EQUALS true
Response envelope{ data, pagination, error.errors[] }{ result, pagination, error{code, message, details[]} }
Campaign fieldsadamId, countriesOrRegions, dailyBudgetAmount, servingStatuspromotedObjectId, targeting{countryOrRegion, supplySource}, dailyBudget.value, bidStrategy, systemStatus / displayStatus
Ad group and keyworddefaultBidAmount, pricingModel CPC, keyword status ACTIVEbidStrategy.bid{amount, currency}, pricingModel CPT, keyword status ENABLED
UpdatesPUT /campaigns/{id} with { campaign: {…} }PUT /campaigns/{id} with only the changed fields, unwrapped. Arrays are replaced whole
Bulk writes…/bulk with an array; duplicates come back as 200 with an error inside…/bulk-create with { items: [{ correlationId, data }], allowPartialSuccess }; success reported per item
ReportsPOST /reports/campaignsPOST /reports/apps/campaigns/query. DAILY granularity covers at most 90 days per request
Rate limitsThinly documentedRateLimit-Limit / -Remaining / -Reset on every response, Retry-After on 429
Lifetime budgetRemoved in v5.6Not on the campaign either. Plan a total as a daily budget plus an end date

Where the live API disagreed with the documentation

These came from read-only calls against our own account in October 2026. Each one would have produced a silent wrong number or a hard failure if we had trusted the docs.

  • The documentation says: The campaign report requires a campaignId filter.

    What the API actually did: A campaignId filter is rejected with 400 INVALID_FIELD_ATTRIBUTE. The filter field is id (a string). With no filter at all, the report returns the whole account in one call.

  • The documentation says: An empty report returns result.rows: [].

    What the API actually did: It returns result: {} with no rows key. Code that reads rows.length throws instead of reporting zero.

  • The documentation says: List responses carry a total count.

    What the API actually did: List endpoints return only offset and pageSize, and pageSize is the number of items returned, not the size you asked for. Read until an empty page.

  • The documentation says: Times are UTC.

    What the API actually did: Campaign reads return times without a zone (UTC). The same campaign's times in report metadata come back shifted into the account's time zone. Read schedules from the campaign, not the report.

  • The documentation says: The ad is the unit of delivery.

    What the API actually did: Campaigns migrated from v5 were serving with zero ads — /ads/query returned nothing. Do not assume every campaign has an ad to copy.

  • The documentation says: Any window can be requested with a granularity.

    What the API actually did: A one-day window is sent without granularity, and that day's figures arrive in totalMetrics instead of the daily rows.

A migration plan that does not risk spend

  1. Inventory every caller of api.searchads.apple.com — scripts, scheduled jobs, BI connectors, vendors. Ask vendors for their cut-over date in writing.
  2. Port the reads first and run them next to v5 against the same account. Compare spend, installs and the campaign list day by day; a difference of zero for two weeks is the bar we used.
  3. Do not create a second connection that also writes or imports spend. Two connectors on one account double-count spend.
  4. Switch writes one account at a time, starting with reversible ones: a budget change you can set back, a pause you can resume.
  5. Finish well before the deadline. We set our own cut-over six weeks early, on December 11, 2026, so a surprise has room to be fixed.

To find the first step's callers in your own code, we published the scanner we used. It lists every v5 call it recognises and what each becomes in the Platform API. Node 18+, no dependencies, nothing leaves your machine; it exits 1 while v5 calls remain, so it can gate CI.

npx github:y7technology/apple-ads-api-v5-migration path/to/your/code

Scanner and notes on GitHub →

How Loop Marketing handles it

Loop Marketing connects to your Apple Ads and Google Ads accounts and changes budgets and bids inside limits you set. Its Apple Ads connector already speaks both APIs and reads them side by side on every cycle, so the switch is per account and reversible. If you would rather not port an in-house script before January, you can connect the account instead.

Free trial. No credit card to begin.