Advertiser Public API migration reference for LLM coding agents
A reference for LLM coding agents migrating a client's integration from the User Acquisition Manage API (legacy) to the Ads Unified Platform public API.
Read time 16 minutesLast updated 7 days ago
Audience: This file is written to be consumed by an LLM coding agent (Claude Code or similar) tasked with migrating a client's integration from the legacy Unity "Advertise API" (, the/advertise/v1/...service) to the new Unity Ads Advertiser Public API (mz-advertiser-public-api). It is split into self-contained entity blocks. Each block states what changed and exactly what to do. Read the Conceptual model change and Global changes sections first — they apply to every block./ads/advertiser/public/v1/...
How to use this file (instructions for the agent)
- Treat the two contracts as the source of truth, not this prose. Legacy =
. New =
mz-advertiser-public-api/openapi/specs/advertise-v1.yml. This guide covers legacy v1 only; if the client callsads-unified-platform/openapi/advertiser/platform-public-v1-routes.yaml, diff that spec (/advertise/v2alpha1/...) separately and surface any v2-only usage to the human.advertise-v2-alpha.yml - Migrate one entity block at a time, in the order given (Apps → Attribution → Creatives →
Creative Packs → Campaign Management → Campaigns → Bids → Budget → Targeting → Assignment). Do
not start a block until the one it depends on compiles/passes. Apps first: everything is keyed by
the new . Attribution follows immediately (it is app-level), and Campaign Management lookups (
applicationId/sdk-event-names) come before Campaigns because they feed campaign setup.eligibility - When a legacy feature is listed under 🚫 No equivalent, do not invent or guess an
endpoint. Stop and surface it to the human as an open question. The most important examples are
(application lifecycle is now owned by USF) and the deprecated retention product area (retention bids/eligibility) — see the blocks.
DELETE app - After each block, re-map field names per the field tables; never assume a field carried over unchanged unless the table says "unchanged".
- Money changed units (dollars → microdollars) and the app identifier changed type (24-char hex → UUID). These two transforms touch almost every block. Get them right first.
Conceptual model change (read this first)
The API was re-hosted onto the unified platform and re-centered from the "campaign set" (app)
as a 24-char Mongo id to the platform (UUID), sub-resource reads were folded into
the campaign object, and the five separate bid endpoints were collapsed into one.
ApplicationLegacy model
Organization└── App (campaignSetId, 24-char hex) ← "apps" in the path ├── Creatives ├── Creative Packs └── Campaign (goal: installs|retention|roas|creativeTesting|eventOptimization) ├── cpi-bids | source-bids | roas-bids | retention-bids | event-optimization-bids │ (5 separate sub-resources, each GET/PUT/PATCH, dollar strings) ├── targeting (GET + PATCH) ├── budget (GET + PATCH, dollars; daily OR dailyPerCountry map) └── assigned-creative-packs (POST/DELETE one at a time)
New model
Organization└── Application (applicationId, UUID) ← "applications" in the path ├── Creatives (now also PATCH + DELETE) ├── Creative Packs ├── attribution (GET + PATCH, app-level) ← NEW placement of attribution └── Campaign (optimizationType: cpi|legacyRetention|roas|appEventConversion; isCreativeTesting) ├── bids (ONE PATCH .../bids; roas|manual|manualSource|appEvent) ├── targeting (PATCH only) ├── budget-cap (PATCH only, microdollars; countryGroupDailyLimits) ├── assign-creative-pack / unassign-creative-pack (list-based PATCH/DELETE) └── (reads of bids/budget/targeting/packs come from GET campaign?includeFields=…)Organization└── campaigns (NEW org-wide campaign list, filter by appIds)
The shifts that drive every transformation below:
# | Shift | Consequence for migration |
|---|---|---|
| 1 | | Resource path segment renamed. The id changes from |
| 2 | Money → microdollars | All bids/budgets were dollar strings ( |
| 3 | 5 bid endpoints → 1 | |
| 4 | Sub-resource reads folded in | Legacy |
| 5 | | Campaign type enum changed and split ( |
| 6 | Attribution relocated | Legacy set attribution URLs on the campaign create body and on the app. New API: campaign update takes |
| 7 | Assignment is list-based | Legacy assigned/unassigned one pack per request. New API replaces/removes with an array of |
Global changes (apply to all endpoints)
Aspect | Legacy | New |
|---|---|---|
| Host | | |
| Path prefix | | |
| App path segment | | |
| Full example | | |
| Auth | HTTP Basic (service-account key) | HTTP Basic still works; Bearer JWT ( |
| Org id | | |
| App id (type change) | | |
| Money | dollar decimal strings ( | microdollars as integer strings/numbers ( |
| Campaign / creative / pack ids | 24-char hex — unchanged (still | 24-char hex — unchanged |
| List envelope | | ⚠️ inconsistent: campaigns & creatives use |
| Pagination params | | |
Authorization (roles/permissions) — read before assuming 403 is a bug
Authentication is unchanged (same Basic credential), but authorization is not: every new
endpoint is gated by permission strings checked at the gateway (visible per-operation in the new
spec under ). Advertiser routes require
permissions (, , ,
, , , …), granted via
org-scoped service-account roles in the Unity Cloud dashboard (e.g. "Advertise API Apps
Editor" grants ). The shared
endpoints accept either or the publisher-side .
x-unity-gateway.authorization.rulesadvertise.*advertise.campaigns.*advertise.creatives.*advertise.creative_packs.*advertise.bids.updateadvertise.targeting.updateadvertise.apps.*advertise.apps.get/list/create/update/delete…/applicationsadvertise.apps.*monetize.projects.*Agent instructions:
- Before migrating, ask the human to confirm the client's service account holds the required advertiser roles for the new API — do not assume legacy-API grants carry over.
- If a request that worked on the legacy API returns 403 on the new one with the same credential, treat it as a missing role grant, not a request bug. Stop and surface it; do not mutate the request trying to "fix" it.
Rate limits (update client-side backoff)
Operation class | Legacy | New |
|---|---|---|
Reads ( | 429 defined; per-resource | 20/s + 8000/hour per resource limiter |
Creates ( | (batched) | 1/s + 60/hour ( |
Updates ( | (batched) | 1/s + 200/hour ( |
| Bids updates | (batched arrays) | 20/s + 12000/hour ( |
| Campaign management reads | — | 6/s + 8000/hour ( |
Master endpoint map
Legacy operation | Legacy route | New operationId | New route | Block |
|---|---|---|---|---|
| | | | A |
| | | | A |
| | | | A |
| | | | A |
| | — | 🚫 no delete-application endpoint | A |
| | | | B |
| | | | B |
| | | | B |
| — | — | | | B |
| — | — | | | B |
| | | | C |
| | | | C |
| | | | C |
| | | | C |
| | | | C |
| | | | D |
| | | | D |
| | | | D |
| | | | D |
| | | | D |
| | | | E |
| …SourceBids | | | | E |
| …RoasBids | | | | E |
| …EventOptimizationBids | | | | E |
| …RetentionBids | | — | 🚫 retention bids deprecated — no longer supported | E |
| | — (read via campaign) | | F |
| | | | F |
| | — (read via campaign) | | G |
| | | | G |
| | — (read via campaign) | | H |
| | | | H |
| | | | H |
| | | | I |
| | | | I |
| | — | 🚫 ROAS no longer needs eligibility; retention deprecated | I |
| — (attribution was on campaign/app) | ( | | | J |
Block A — Apps → Applications
What changed
- Path segment →
apps; idapplications(24-char hex) →campaignSetId(UUID). This is the highest-risk remap — every campaign/creative/pack call is nested under it. Build and persist aapplicationIdmap during migration.legacy campaignSetId → new applicationId - is now a required, explicit field on create (was implied by
platform).store - App-level attribution URLs (/
appAttributionClickUrl) moved to the dedicated attribution endpoint — see Block J.appAttributionStartUrl - has no equivalent (confirmed intentional). Application lifecycle moved out of the advertiser scope to Unity Services Foundation (USF), and application deletes are currently disabled platform-wide. The new applications resource exposes list/create/get/update only. If the client deletes apps via the API, stop and surface it.
DELETE app - The list changed shape and lost pagination/filters. Legacy returned
GET …/appswith{ total, offset, limit, results[] }/offsetandlimit/filter[store]query params. Newfilter[storeId]returns a bare JSON array of all the org's applications — no envelope, no pagination, no filters. Drop the params and filter/paginate client-side.GET …/applications
Field mapping — create (advertise-createApp
→ CreateApplicationPublicDto
)
advertise-createAppCreateApplicationPublicDtoLegacy field | New field | Notes |
|---|---|---|
| | unchanged |
| | Remap enum → |
| | unchanged value |
| — | | New, required. Derive from the legacy |
| — | 🚫 no field on the app response. Surface if the client relied on it. |
| | present on the response; not a create input — set server-side. |
| → attribution | Moved. Set via |
| → attribution | Moved. Set via |
| — | | new optional privacy object |
| — | | new optional grouping (shared publisher/advertiser app model) |
New enum: , , , , ,
, , , . New enum: , , ,
, , , , , , , , , .
storeGooglePlayAppleAppStoreSamsungGalaxyAmazonAppStoreMacAppStoreUDPMicrosoftStoreHuaweiAppGalleryAPKplatformAndroidiOSOSXWindowsLinuxWebGLWindows_StorePS4PS5XboxOnetvOSSwitchVisionOSLegacy → new (confirmed): → ; →
; → depends on the actual store — Samsung →
, Huawei → ; when no store is identifiable
the platform maps it to .
store{store, platform}apple{AppleAppStore, iOS}google{GooglePlay, Android}standalone_android{SamsungGalaxy, Android}{HuaweiAppGallery, Android}{APK, Android}Field mapping — response (app
→ ApplicationPublicResponseDto
)
appApplicationPublicResponseDtoLegacy field | New field | Notes |
|---|---|---|
| | Type change. Persist the mapping. |
| | unchanged |
| | enum remap (above) |
| | unchanged (integer, nullable) |
| — (see | moved to attribution resource |
| — | 🚫 gone |
| — (not on the DTO) | not exposed on the app response |
| — | | new |
⚠️ ID remap is the highest-risk step. Persistand rewrite every downstream path that usedlegacy campaignSetId → new {applicationId}.campaignSetId
Block B — Creatives
What changed
- Nested under instead of
applications/{applicationId}.apps/{campaignSetId} - Still ,
multipart/form-dataJSON first, then the binary file field(s).creativeInfo - New operations: (rename) and
PATCH …/creatives/{creativeId}.DELETE …/creatives/{creativeId} - enum values were renamed (snake_case). Re-map.
type - Image upload limit raised to 20 MB (was 5 MB); playables 10 MB.
- Video uploads () work exactly as in legacy (confirmed by API owners). The OpenAPI schema documenting the video-creation variant landed in late July 2026 — if your copy of the new spec lacks a video
videoFilevariant, refresh the spec rather than assuming videos are unsupported.oneOf
type
enum remap
typeLegacy | New |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| 🚫 not supported — only portrait/landscape video exist. Surface. |
| — | |
status
enum
statusUnchanged set: , , , , ,
.
uploadedprocessingprocessingFailedpendingModerationapprovedrejectedmultipart field names
creativeInfosquareEndCardFileportraitEndCardFilelandscapeEndCardFileplayableFilecreativeInfofileNameorientationlandscapeportraitbothCreativePublicResponseDtoidnamelanguagetypestatusfiles[]{name,url}createdAtupdatedAtBlock C — Creative Packs
What changed
- Nested under . CRUD verbs are the same (GET/POST/GET/PATCH/DELETE).
applications/{applicationId} - gains a
typevalue:banner,video,playable,video+playable.banner - List envelope is the legacy-style (unlike campaigns/creatives).
{ total, offset, limit, results[] } - The response is richer: adds ,
applicationId(moderationStatus/pending/approved, nullable),rejected(nullable),ageRating,assignedCampaignIds[]/createdAt.updatedAt
Field mapping — create (createCreativePack
→ CreateCreativePackPublicDto
)
createCreativePackCreateCreativePackPublicDtoLegacy field | New field | Notes |
|---|---|---|
| | unchanged |
| | unchanged (24-char hex creative ids) |
| | same values + new |
| | unchanged (nullable) |
| | unchanged (UUID, nullable) |
Field mapping — response
Legacy field | New field | Notes |
|---|---|---|
| | 24-char hex — unchanged |
| same | unchanged |
| | renamed |
| — | | new |
Block D — Campaigns
What changed
- Nested under . Get/Update/Delete are per-app; listing is now org-wide (
applications/{applicationId}, filterGET …/organizations/{orgId}/campaigns) — there is no per-app list.appIds - →
goal(+optimizationTypeboolean +isCreativeTesting).appEventType - Attribution URLs are no longer part of campaign create. Set them after create via campaign
(
PATCH) or the app attribution endpoint (Block J).attributionLinks - is not a create field — create takes only
scheduleEnd; setstartviaend.PATCH - supports
GET campaign(includeFields,bids,budget,creativePacks) — this is how you now read bids/budget/assigned packs.application - values are uppercase and expanded:
postInstallWindow,D0,D1,D3,D7,D14.D28
goal
→ optimizationType
remap
goaloptimizationTypeLegacy | New |
|---|---|
| |
| |
| |
| |
| |
Field mapping — create (createCampaign
→ CreateCampaignDto
)
createCampaignCreateCampaignDtoLegacy | New |
|---|---|
| |
| |
| |
| |
| |
Field mapping — update (updateCampaign
→ UpdateCampaignPublicDto
)
updateCampaignUpdateCampaignPublicDtoLegacy field | New field | Notes |
|---|---|---|
| | unchanged |
| | date → datetime; |
| | inverted sense — |
| | uppercased |
| | now a nested object; |
⚠️ The update response is a slimmer object (:CampaignPublicDetailsResponseDto,id,name,start,end,autoStart,paused), not the full campaign. If the client reads other campaign fields off the PATCH response, re-fetch viaattributionLinks.GET campaign
Field mapping — response (legacy campaign
→ CampaignPublicResponseDto
)
campaignCampaignPublicResponseDtoLegacy field | New field | Notes |
|---|---|---|
| | 24-char hex — unchanged |
| | unchanged |
| | remap table above (reverse direction) |
| | ⚠️ inverted sense on reads too — |
| — | 🚫 not on the new response. Surface if the client branches on it. |
| | date → datetime; |
| | renamed + value change ( |
| | renamed; only present with |
| — | 🚫 retention bids deprecated — no write support; treat any read value as legacy-only |
| | only with |
| | enum rename; single → array |
| | uppercased; |
| | unchanged |
| — | | new — see the new spec |
Reads via includeFields
includeFieldsLegacy read of a campaign's bids/budget/assigned packs used separate GETs. Now:
. Bid arrays,
, , and full come back on the campaign object.
GET …/campaigns/{campaignId}?includeFields=bids,budget,creativePacks,applicationbudgetcreativePackIds[]applicationBlock E — Bids (five endpoints → one)
What changed
The five legacy sub-resources (, , , ,
), each with // over dollar strings, collapse into a
single over a typed body in microdollars. There is
no GET — read current bids via .
cpi-bidssource-bidsroas-bidsretention-bidsevent-optimization-bidsGETPUTPATCHPATCH …/campaigns/{campaignId}/bidsGET campaign?includeFields=bidsEndpoint + body remap
Legacy sub-resource | New body field in | Per-entry shape (new) |
|---|---|---|
| | |
| | |
| | |
| | |
| 🚫 deprecated — no support | legacy |
Money transform (critical)
Legacy dollar string → new microdollars: multiply by 1_000_000. → . ROAS
/ (confirmed): legacy ROAS was a percentage string ( = 12.34%);
the new is a decimal ratio — divide by 100 ( → ; 10% → ).
Semantics (confirmed by API owners):
"0.201"201000goaltargetgoal"12.34"target"12.34"0.12340.10Legacy PUT/PATCH model | New model |
|---|---|
| The single |
Clearing bids (confirmed): send an empty array for that bid type in the — same
effect as the legacy .
[]PATCHPUT []⚠️ Because the newreplaces rather than merges, a naive port of legacyPATCH(merge) calls will silently drop every bid not included in the request. Convert legacy merge-style updates to full-set writes: read current bids viaPATCH, apply the delta locally, and send the complete array.GET campaign?includeFields=bids
Block F — Budget → Budget Cap
What changed
- is gone → read via
GET …/budget.GET campaign?includeFields=budget - →
PATCH …/budget.PATCH …/campaigns/{campaignId}/budget-cap - Dollars → microdollars (strings).
- The per-country map () is replaced by named country groups (
dailyPerCountry: { "US": "123.45", … }).countryGroupDailyLimits: [{ name, countries[], limit }] - New explicit type discriminators on the response: (
campaignLimitType/unlimited),total(dailyLimitType/daily/countryDaily).unlimited
Field mapping (budget
→ PatchBudgetCapDto
)
budgetPatchBudgetCapDtoLegacy field | New field | Notes |
|---|---|---|
| | ×1e6. Cap representation semantics carry over from legacy (per API owners): |
| | ×1e6 |
| | Restructure: each |
| — (read via campaign budget) | present on the campaign budget read, not the PATCH response |
Block G — Targeting
What changed
- is gone → read targeting fields off
GET …/targeting.GET campaign - remains but the body is restructured and most legacy device fields are dropped (they were already
PATCH …/targetingin the legacy spec).deprecated
Field mapping (targetingOptions
→ UpdateCampaignPublicTargetingDto
)
targetingOptionsUpdateCampaignPublicTargetingDtoLegacy field | New field | Notes |
|---|---|---|
| | renamed (source app ids) |
| | now provider-specific category lists |
| | restructured |
| | restructured |
| | now subdivision-id based |
| — | 🚫 removed (confirmed — cannot be configured on unified). Delete the client code that sets them; mention the drop to the human. |
Block H — Assigned Creative Packs (now list-based)
What changed
- Legacy assigned/unassigned one pack per request (/
POST).DELETE …/assigned-creative-packs - New API operates on a list:
- with
PATCH …/campaigns/{campaignId}/assign-creative-pack— replaces the assigned set with the provided ids.{ creativePackIds: [...] } - with
DELETE …/campaigns/{campaignId}/unassign-creative-pack— removes the listed ids.{ creativePackIds: [...] }
- List of currently-assigned packs: (
GET campaign?includeFields=creativePacks).creativePackIds[]
Migration approach
Collect all packs the client assigned to a campaign, then make one
with the full array (instead of N s). Semantics confirmed: the endpoint replaces the
complete assignment with the provided — always send the full desired set, or
packs omitted from the array are unassigned.
PATCH assign-creative-packPOSTcreativePackIdsBlock I — Audience Pinpointer → Campaign Management
What changed
Legacy | New |
|---|---|
| |
| |
| 🚫 removed — ROAS campaigns no longer require eligibility; delete any gating logic |
| 🚫 removed — retention is deprecated entirely; surface active retention usage |
sdk-event-names{ total, results[]{ eventOptimizationType, sdkEventNames[] } }eligibility{ total, results[]{ eventOptimizationType, countries[] } }Block J — Attribution (relocated)
What changed
Attribution URLs were previously set on the app (/) and passed
into campaign create. The new API has a dedicated app-level attribution resource plus a
per-campaign on campaign update.
appAttributionClickUrlStartUrlattributionLinks- →
GET …/applications/{applicationId}/attribution.{ id, startUrl, clickUrl, viewUrl?, partner, type?, createdAt, updatedAt } - with
PATCH …/applications/{applicationId}/attribution.{ startUrl?, clickUrl?, viewUrl? }
partnerotheradjustairbridgeappsflyerbranchbytebrewkochavamailrusingulartenjinyandextypeuserLevelnonUserLevelmixedMigration approach
- Move app-level /
appAttributionClickUrl→appAttributionStartUrl(PATCH …/attribution/clickUrl).startUrl - Move campaign-create /
attributionClickUrl→ aattributionStartUrlon the campaign (PATCH) after creating the campaign.attributionLinks.{clickUrl,startUrl,viewUrl} - is new (optional).
viewUrland other URL macros: validation rules come from a shared attribution-verification library used by both the legacy and new APIs (confirmed), so macro behavior is unchanged — keep legacy URLs as-is.{ifa}
Suggested migration algorithm (deterministic order for the agent)
- Config: swap host to , path prefix
…/ads/advertiser→/advertise/v1, and the/public/v1segment →apps. Keep Basic auth.applications - Apps (Block A): for each legacy app, (remap
POST …/applications→store). Persist{store, platform}. Do not send attribution URLs here.legacy campaignSetId → new applicationId - Attribution (Block J): with the old app-level URLs.
PATCH …/applications/{id}/attribution - Creatives (Block B): re-upload/re-map, translating the enum. Persist
typeif ids are re-minted (verify whether ids are preserved).legacy creativeId → new creativeId - Creative Packs (Block C): create packs referencing new creative ids.
- Campaign Management (Block I): re-point audience-pinpointer lookups —
→
…/audience-pinpointer/sdk-event-names, and…/applications/{id}/sdk-event-names→…/audience-pinpointer/event-optimization-info. Read-only; use the results to inform campaign…/applications/{id}/eligibility/appEventTypeand eligible countries. ROAS-info gating logic can simply be deleted (eligibility no longer required); retention-info is gone with the deprecated retention product (see step 13).sdkEventNames - Campaigns (Block D): create with (+
optimizationType/isCreativeTesting/appEventType),roasTypeonly. Thenstartto setPATCH,end,attributionLinks,autoStart.paused - Bids (Block E): convert dollars → microdollars (ROAS targets: percent → decimal ratio,
÷100); send one with the complete desired set per bid type (the PATCH replaces, never merges;
PATCH …/bidsclears). Retention bids are deprecated — do not migrate them; surface any usage.[] - Budget (Block F): in microdollars; convert
PATCH …/budget-capmap →dailyPerCountry.countryGroupDailyLimits[] - Targeting (Block G): restructure into /
appleDevice/source+category lists; drop removed device fields (surface them).androidDevice - Assignment (Block H): one list-based per campaign.
PATCH assign-creative-pack - Reads: replace all with
GET …/{bids,budget,targeting,assigned-creative-packs}.GET campaign?includeFields=… - Removed features (surface to human): (app lifecycle owned by USF; deletes currently disabled), the deprecated retention product (bids + eligibility), ROAS eligibility gating (no longer required — delete it), legacy deprecated targeting fields,
DELETE app, andadomaincreatives.squareVideo
Validation checklist (run after migration)
- No request path contains ,
/advertise/v1,/apps/,/cpi-bids,/source-bids,/roas-bids,/retention-bids,/event-optimization-bids,/audience-pinpointer, or/assigned-creative-packs(now…/budget).budget-cap - Every app path uses the new UUID , not the 24-char hex
applicationId.campaignSetId - All money values are microdollars (integers), never dollar decimals like .
"2500.20" - Bids go to the single ; reads use
PATCH …/bids.GET campaign?includeFields=bids - Every bid sends the complete set for its bid type (replace semantics — partial sends drop the omitted bids); legacy merge-style updates were converted to read-modify-write.
PATCH - ROAS targets were converted from percent strings to decimal ratios (→
"12.34").0.1234 - Budget uses +
budget-cap(nocountryGroupDailyLimits[]map).dailyPerCountry - Campaign was remapped to
goal(+optimizationType), and attribution URLs are set viaisCreativeTesting/attributionLinks, not campaign create.…/attribution - Creative uses the new snake_case values (
type,end_card_square, …).video_portrait - Every read of legacy was remapped to
enabledwith the sense inverted (paused⇒enabled:true) — on list, get, and update responses alike.paused:false - Nothing parses full campaign fields off the campaign response (it returns the slim details object); full state is re-fetched via
PATCH.GET campaign?includeFields=… - Application list parsing expects a bare array (no envelope) and no longer sends
{total, results}/offset/limit/filter[store].filter[storeId] - Assignment uses one list-based call per campaign, not N single assigns.
- , deprecated retention usage (bids/eligibility),
DELETE appcreatives, and any removed targeting fields were surfaced to the human as gaps (if the client used them); ROAS eligibility gating was deleted.squareVideo
Quick reference: 🚫 removed / no-equivalent (do not migrate or fabricate — all confirmed by API owners)
- — no delete-application endpoint; app lifecycle is owned by USF and deletes are currently disabled platform-wide.
DELETE app - ROAS-info — removed; ROAS campaigns no longer require eligibility. Delete gating logic.
- Retention (whole product area) — deprecated: and retention bids have no successor. Surface active retention campaigns/bids.
retention-info - GET bids / budget / targeting / assigned-packs — replaced by .
GET campaign?includeFields=… - Legacy deprecated targeting fields (,
limitedAdTracking,connectionType,screenSize,screenDensity,allowedDevices) and apposMax— cannot be configured on unified; delete the client code.adomain - creative type — not supported; only
squareVideo/video_portrait.video_landscape