Documentation

Unity Monetization

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 38 minutesLast updated a month 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" (
/advertise/v1/...
, the
mz-advertiser-public-api
service) to the new Unity Ads Advertiser Public API (
/ads/advertiser/public/v1/...
). 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.

How to use this file (instructions for the agent)

  1. Treat the two contracts as the source of truth, not this prose. Legacy =
    mz-advertiser-public-api/openapi/specs/advertise-v1.yml
    . New =
    ads-unified-platform/openapi/advertiser/platform-public-v1-routes.yaml
    . This guide covers legacy v1 only; if the client calls
    /advertise/v2alpha1/...
    , diff that spec (
    advertise-v2-alpha.yml
    ) separately and surface any v2-only usage to the human.
  2. 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
    applicationId
    . Attribution follows immediately (it is app-level), and Campaign Management lookups (
    sdk-event-names
    /
    eligibility
    ) come before Campaigns because they feed campaign setup.
  3. 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
    DELETE app
    (application lifecycle is now owned by USF) and the deprecated retention product area (retention bids/eligibility) — see the blocks.
  4. After each block, re-map field names per the field tables; never assume a field carried over unchanged unless the table says "unchanged".
  5. 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
Application
(UUID)
, sub-resource reads were folded into the campaign object, and the five separate bid endpoints were collapsed into one.

Legacy 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
apps
→
applications
Resource path segment renamed. The id changes from
campaignSetId
(24-char hex) to
applicationId
(UUID). Build a
legacy campaignSetId → new applicationId
lookup.
2Money → microdollarsAll bids/budgets were dollar strings (
"0.201"
,
"2500.20"
). New API is integer microdollars (1 USD = 1_000_000). Multiply by 1e6.
35 bid endpoints → 1
cpi/source/roas/retention/event-optimization-bids
collapse into a single
PATCH …/bids
with a typed body. No GET bids — read via
GET campaign?includeFields=bids
.
4Sub-resource reads folded inLegacy
GET …/budget
,
GET …/targeting
,
GET …/*-bids
,
GET …/assigned-creative-packs
are gone. Read them from
GET campaign?includeFields=bids,budget,creativePacks
.
5
goal
→
optimizationType
Campaign type enum changed and split (
creativeTesting
became the
isCreativeTesting
boolean;
eventOptimization
became
appEventConversion
+
appEventType
).
6Attribution relocatedLegacy set attribution URLs on the campaign create body and on the app. New API: campaign update takes
attributionLinks
, and there is a dedicated app-level
GET/PATCH …/attribution
. Campaign create no longer accepts attribution URLs.
7Assignment is list-basedLegacy assigned/unassigned one pack per request. New API replaces/removes with an array of
creativePackIds
in one call.

Global changes (apply to all endpoints)

Aspect

Legacy

New

Host
https://services.api.unity.com
https://services.api.unity.com/ads/advertiser
(prod) ·
https://staging.services.api.unity.com/ads/advertiser
(staging)
Path prefix
/advertise/v1
/public/v1
App path segment
…/organizations/{orgId}/apps/{campaignSetId}
…/organizations/{organizationId}/applications/{applicationId}
Full example
…/advertise/v1/organizations/{orgId}/apps/{campaignSetId}/campaigns
…/ads/advertiser/public/v1/organizations/{orgId}/applications/{applicationId}/campaigns
AuthHTTP Basic (service-account key)HTTP Basic still works; Bearer JWT (
unity
/
ads
issuers) also accepted. Credential unchanged, but the required roles/permissions differ — see "Authorization" below.
Org id
organizationId
(int64) in path
organizationId
(Genesis id, numeric) in path — unchanged
App id (type change)
campaignSetId
— 24-char hex (
^[0-9a-fA-F]{24}$
)
applicationId
— UUID
Moneydollar decimal strings (
"2500.20"
, bid
"0.201"
)
microdollars as integer strings/numbers (
"2500200000"
); 1 USD = 1_000_000
Campaign / creative / pack ids24-char hex — unchanged (still
^[a-f0-9]{24}$
)
24-char hex — unchanged
List envelope
{ total, offset, limit, results[] }
everywhere
⚠️ inconsistent: campaigns & creatives use
{ items[], meta:{ total, limit, offset } }
; creative-packs use
{ total, offset, limit, results[] }
; applications return a bare JSON array (no envelope, no pagination). Parse per endpoint.
Pagination params
offset
(0),
limit
(max 1000, default 1000) — on every list
offset
(0),
limit
(default 100) on paginated lists only (campaigns, creatives, creative-packs);
GET …/applications
takes neither
— drop the params there (Block A)

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
x-unity-gateway.authorization.rules
). Advertiser routes require
advertise.*
permissions (
advertise.campaigns.*
,
advertise.creatives.*
,
advertise.creative_packs.*
,
advertise.bids.update
,
advertise.targeting.update
,
advertise.apps.*
, …), granted via org-scoped service-account roles in the Unity Cloud dashboard (e.g. "Advertise API Apps Editor" grants
advertise.apps.get/list/create/update/delete
). The shared
…/applications
endpoints accept either
advertise.apps.*
or the publisher-side
monetize.projects.*
.
Agent instructions:
  1. 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.
  2. 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 (
GET
)
429 defined; per-resource20/s + 8000/hour per resource limiter
Creates (
POST
)
(batched)1/s + 60/hour (
advertiseCreate
)
Updates (
PATCH
/
DELETE
)
(batched)1/s + 200/hour (
advertiseModify
)
Bids updates(batched arrays)20/s + 12000/hour (
advertiseBids
) + modify tier
Campaign management reads—6/s + 8000/hour (
advertiseCampaignManagement
)

Master endpoint map

Legacy operation

Legacy route

New operationId

New route

Block

advertise-listApps
GET …/apps
publicOrganizationApplications
GET …/applications
A
advertise-createApp
POST …/apps
publicCreateApplication
POST …/applications
A
advertise-getApp
GET …/apps/{campaignSetId}
publicGetApplicationById
GET …/applications/{applicationId}
A
advertise-updateApp
PATCH …/apps/{campaignSetId}
publicUpdateApplication
PATCH …/applications/{applicationId}
A
advertise-deleteApp
DELETE …/apps/{campaignSetId}
—🚫 no delete-application endpointA
advertise-listCreatives
GET …/apps/{id}/creatives
publicGetCreatives
GET …/applications/{id}/creatives
B
advertise-createCreative
POST …/apps/{id}/creatives
(multipart)
publicCreateCreative
POST …/applications/{id}/creatives
(multipart)
B
advertise-getCreative
GET …/creatives/{creativeId}
publicGetCreativeById
GET …/applications/{id}/creatives/{creativeId}
B
——
publicUpdateCreativeById
PATCH …/creatives/{creativeId}
(NEW: rename)
B
——
publicDeleteCreativeById
DELETE …/creatives/{creativeId}
(NEW)
B
advertise-listCreativePacks
GET …/creative-packs
publicGetCreativePacks
GET …/applications/{id}/creative-packs
C
advertise-createCreativePack
POST …/creative-packs
publicCreateCreativePack
POST …/applications/{id}/creative-packs
C
advertise-getCreativePack
GET …/creative-packs/{packId}
publicGetCreativePackById
GET …/applications/{id}/creative-packs/{creativePackId}
C
advertise-updateCreativePack
PATCH …/creative-packs/{packId}
publicUpdateCreativePack
PATCH …/creative-packs/{creativePackId}
C
advertise-deleteCreativePack
DELETE …/creative-packs/{packId}
publicDeleteCreativePack
DELETE …/creative-packs/{creativePackId}
C
advertise-listCampaigns
(per app)
GET …/apps/{id}/campaigns
publicGetCampaigns
(org-wide)
GET …/organizations/{orgId}/campaigns?appIds={applicationId}
D
advertise-createCampaign
POST …/apps/{id}/campaigns
publicCreateCampaign
POST …/applications/{id}/campaigns
D
advertise-getCampaign
GET …/campaigns/{campaignId}
publicGetCampaignById
GET …/applications/{id}/campaigns/{campaignId}?includeFields=…
D
advertise-updateCampaign
PATCH …/campaigns/{campaignId}
publicUpdateCampaign
PATCH …/applications/{id}/campaigns/{campaignId}
D
advertise-deleteCampaign
DELETE …/campaigns/{campaignId}
publicDeleteCampaignById
DELETE …/applications/{id}/campaigns/{campaignId}
D
advertise-list/replace/patchCpiBids
GET/PUT/PATCH …/campaigns/{id}/cpi-bids
publicUpdateBids
(
manualBids
)
PATCH …/campaigns/{campaignId}/bids
E
…SourceBids
…/source-bids
publicUpdateBids
(
manualSourceBids
)
PATCH …/campaigns/{campaignId}/bids
E
…RoasBids
…/roas-bids
publicUpdateBids
(
roasBids
)
PATCH …/campaigns/{campaignId}/bids
E
…EventOptimizationBids
…/event-optimization-bids
publicUpdateBids
(
appEventBids
)
PATCH …/campaigns/{campaignId}/bids
E
…RetentionBids
…/retention-bids
—🚫 retention bids deprecated — no longer supportedE
advertise-getBudget
GET …/campaigns/{id}/budget
— (read via campaign)
GET campaign?includeFields=budget
F
advertise-updateBudget
PATCH …/campaigns/{id}/budget
publicPatchCampaignBudgetCap
PATCH …/campaigns/{campaignId}/budget-cap
F
advertise-getTargeting
GET …/campaigns/{id}/targeting
— (read via campaign)
GET campaign
(targeting fields on the response)
G
advertise-updateTargeting
PATCH …/campaigns/{id}/targeting
publicPatchCampaignTargeting
PATCH …/campaigns/{campaignId}/targeting
G
advertise-listAssignedCreativePacks
GET …/campaigns/{id}/assigned-creative-packs
— (read via campaign)
GET campaign?includeFields=creativePacks
H
advertise-assignCreativePack
(one)
POST …/assigned-creative-packs
publicAssignCreativePacks
(list)
PATCH …/campaigns/{campaignId}/assign-creative-pack
H
advertise-unassignCreativePack
(one)
DELETE …/assigned-creative-packs/{id}
publicUnassignCreativePacks
(list)
DELETE …/campaigns/{campaignId}/unassign-creative-pack
H
advertise-listSdkEventNames
GET …/audience-pinpointer/sdk-event-names
publicGetSdkEventNames
GET …/applications/{id}/sdk-event-names
I
advertise-listEventOptInfo
GET …/audience-pinpointer/event-optimization-info
publicGetCampaignManagementEligibility
GET …/applications/{id}/eligibility
I
advertise-listRoasInfo
/
…RetentionInfo
GET …/audience-pinpointer/roas-info
/
…/retention-info
—🚫 ROAS no longer needs eligibility; retention deprecatedI
— (attribution was on campaign/app)(
attributionClickUrl
/
attributionStartUrl
)
publicGetApplicationAttribution
/
publicUpdateApplicationAttribution
GET/PATCH …/applications/{id}/attribution
J

Block A — Apps → Applications

What changed

  • Path segment
    apps
    →
    applications
    ; id
    campaignSetId
    (24-char hex) →
    applicationId
    (UUID). This is the highest-risk remap — every campaign/creative/pack call is nested under it. Build and persist a
    legacy campaignSetId → new applicationId
    map during migration.
  • platform
    is now a required, explicit field on create (was implied by
    store
    ).
  • App-level attribution URLs (
    appAttributionClickUrl
    /
    appAttributionStartUrl
    ) moved to the dedicated attribution endpoint — see Block J.
  • DELETE app
    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.
  • The list changed shape and lost pagination/filters. Legacy
    GET …/apps
    returned
    { total, offset, limit, results[] }
    with
    offset
    /
    limit
    and
    filter[store]
    /
    filter[storeId]
    query params. New
    GET …/applications
    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.

Field mapping — create (
advertise-createApp
→
CreateApplicationPublicDto
)

Legacy field

New field

Notes

name
(required)
name
(required)
unchanged
store
(
apple
/
google
/
standalone_android
)
store
+
platform
Remap enum →
AppleAppStore
/
GooglePlay
/… and set
platform
(
iOS
/
Android
/…).
storeId
(required)
storeId
(optional)
unchanged value
—
platform
(required, enum)
New, required. Derive from the legacy
store
.
adomain
—🚫 no field on the app response. Surface if the client relied on it.
gameId
(integer)
gameId
(integer, nullable)
present on the response; not a create input — set server-side.
appAttributionClickUrl
→ attribution
clickUrl
Moved. Set via
PATCH …/attribution
after create (Block J).
appAttributionStartUrl
→ attribution
startUrl
Moved. Set via
PATCH …/attribution
(Block J).
—
privacy.coppa
,
privacy.mixedAudience
new optional privacy object
—
projectId
/
projectName
new optional grouping (shared publisher/advertiser app model)
New
store
enum:
GooglePlay
,
AppleAppStore
,
SamsungGalaxy
,
AmazonAppStore
,
MacAppStore
,
UDP
,
MicrosoftStore
,
HuaweiAppGallery
,
APK
. New
platform
enum:
Android
,
iOS
,
OSX
,
Windows
,
Linux
,
WebGL
,
Windows_Store
,
PS4
,
PS5
,
XboxOne
,
tvOS
,
Switch
,
VisionOS
.
Legacy
store
→ new
{store, platform}
(confirmed):
apple
→
{AppleAppStore, iOS}
;
google
→
{GooglePlay, Android}
;
standalone_android
→ depends on the actual store — Samsung →
{SamsungGalaxy, Android}
, Huawei →
{HuaweiAppGallery, Android}
; when no store is identifiable the platform maps it to
{APK, Android}
.

Field mapping — response (
app
→
ApplicationPublicResponseDto
)

Legacy field

New field

Notes

id
(24-char hex,
campaignSetId
)
id
(UUID)
Type change. Persist the mapping.
name
name
unchanged
store
/
storeId
store
/
storeId
enum remap (above)
gameId
gameId
unchanged (integer, nullable)
appAttributionClickUrl
/
StartUrl
— (see
…/attribution
)
moved to attribution resource
adomain
—🚫 gone
createdAt
/
updatedAt
— (not on the DTO)not exposed on the app response
—
platform
,
iconUrl
,
projectId
,
testMode
,
kidsSettings
,
coppa
,
mixedAudience
new
⚠️ ID remap is the highest-risk step. Persist
legacy campaignSetId → new {applicationId}
and rewrite every downstream path that used
campaignSetId
.

Block B — Creatives

What changed

  • Nested under
    applications/{applicationId}
    instead of
    apps/{campaignSetId}
    .
  • Still
    multipart/form-data
    ,
    creativeInfo
    JSON first, then the binary file field(s).
  • New operations:
    PATCH …/creatives/{creativeId}
    (rename) and
    DELETE …/creatives/{creativeId}
    .
  • type
    enum values were renamed
    (snake_case). Re-map.
  • Image upload limit raised to 20 MB (was 5 MB); playables 10 MB.
  • Video uploads (
    videoFile
    ) 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
    oneOf
    variant, refresh the spec rather than assuming videos are unsupported.

type
enum remap

Legacy
type

New
type

squareEndCard
end_card_square
endCardPair
end_card_pair
portraitVideo
video_portrait
landscapeVideo
video_landscape
responsivePlayable
playable_responsive
portraitPlayable
playable_portrait
landscapePlayable
playable_landscape
squareVideo
🚫 not supported — only portrait/landscape video exist. Surface.
—
banner
(new)

status
enum

Unchanged set:
uploaded
,
processing
,
processingFailed
,
pendingModeration
,
approved
,
rejected
.

multipart field names

creativeInfo
(JSON, first) + one of:
squareEndCardFile
,
portraitEndCardFile
+
landscapeEndCardFile
, or
playableFile
.
creativeInfo
sub-objects reference the file by
fileName
; playables also carry
orientation
(
landscape
/
portrait
/
both
). Response is
CreativePublicResponseDto
(
id
,
name
,
language
,
type
,
status
,
files[]{name,url}
,
createdAt
,
updatedAt
).

Block C — Creative Packs

What changed

  • Nested under
    applications/{applicationId}
    . CRUD verbs are the same (GET/POST/GET/PATCH/DELETE).
  • type
    gains a
    banner
    value:
    video
    ,
    playable
    ,
    video+playable
    ,
    banner
    .
  • List envelope is the legacy-style
    { total, offset, limit, results[] }
    (unlike campaigns/creatives).
  • The response is richer: adds
    applicationId
    ,
    moderationStatus
    (
    pending
    /
    approved
    /
    rejected
    , nullable),
    ageRating
    (nullable),
    assignedCampaignIds[]
    ,
    createdAt
    /
    updatedAt
    .

Field mapping — create (
createCreativePack
→
CreateCreativePackPublicDto
)

Legacy field

New field

Notes

name
(required)
name
(required)
unchanged
creativeIds[]
(1–3)
creativeIds[]
unchanged (24-char hex creative ids)
type
type
same values + new
banner
androidStoreListing
androidStoreListing
unchanged (nullable)
appleProductPageId
appleProductPageId
unchanged (UUID, nullable)

Field mapping — response

Legacy field

New field

Notes

id
id
24-char hex — unchanged
name
,
type
,
creativeIds
sameunchanged
campaignIds
assignedCampaignIds
renamed
—
applicationId
,
moderationStatus
,
ageRating
,
createdAt
,
updatedAt
new

Block D — Campaigns

What changed

  • Nested under
    applications/{applicationId}
    . Get/Update/Delete are per-app; listing is now org-wide (
    GET …/organizations/{orgId}/campaigns
    , filter
    appIds
    ) — there is no per-app list.
  • goal
    →
    optimizationType
    (+
    isCreativeTesting
    boolean +
    appEventType
    ).
  • Attribution URLs are no longer part of campaign create. Set them after create via campaign
    PATCH
    (
    attributionLinks
    ) or the app attribution endpoint (Block J).
  • scheduleEnd
    is not a create field
    — create takes only
    start
    ; set
    end
    via
    PATCH
    .
  • GET campaign
    supports
    includeFields
    (
    bids
    ,
    budget
    ,
    creativePacks
    ,
    application
    ) — this is how you now read bids/budget/assigned packs.
  • postInstallWindow
    values are uppercase and expanded:
    D0
    ,
    D1
    ,
    D3
    ,
    D7
    ,
    D14
    ,
    D28
    .

goal
→
optimizationType
remap

Legacy
goal

New

installs
optimizationType: cpi
retention
optimizationType: legacyRetention
— ⚠️ retention is deprecated: bids/eligibility are unsupported. Do not create new retention campaigns; surface active ones.
roas
optimizationType: roas
(+
roasType
)
eventOptimization
optimizationType: appEventConversion
(+
appEventType
)
creativeTesting
isCreativeTesting: true
+ base manual optimization (per API owners the only supported base today — i.e. manual CPI:
optimizationType: cpi
,
cpiType: manual
)

Field mapping — create (
createCampaign
→
CreateCampaignDto
)

Legacy
goal

New

installs
optimizationType: cpi
retention
optimizationType: legacyRetention
— ⚠️ retention is deprecated: bids/eligibility are unsupported. Do not create new retention campaigns; surface active ones.
roas
optimizationType: roas
(+
roasType
)
eventOptimization
optimizationType: appEventConversion
(+
appEventType
)
creativeTesting
isCreativeTesting: true
+ base manual optimization (per API owners the only supported base today — i.e. manual CPI:
optimizationType: cpi
,
cpiType: manual
)

Field mapping — update (
updateCampaign
→
UpdateCampaignPublicDto
)

Legacy field

New field

Notes

name
name
unchanged
scheduleStart
/
scheduleEnd
start
/
end
date → datetime;
end
nullable
enabled
(boolean)
paused
(boolean)
inverted sense —
enabled:true
≈
paused:false
autoStart
autoStart
(
ENABLED
/
DISABLED
)
uppercased
attributionClickUrl
/
StartUrl
attributionLinks.{clickUrl,startUrl,viewUrl}
now a nested object;
viewUrl
is new
⚠️ The update response is a slimmer object (
CampaignPublicDetailsResponseDto
:
id
,
name
,
start
,
end
,
autoStart
,
paused
,
attributionLinks
), not the full campaign. If the client reads other campaign fields off the PATCH response, re-fetch via
GET campaign
.

Field mapping — response (legacy
campaign
→
CampaignPublicResponseDto
)

Legacy field

New field

Notes

id
id
24-char hex — unchanged
name
,
billingType
name
,
billingType
unchanged
goal
optimizationType
(+
isCreativeTesting
,
appEventType
,
roasType
)
remap table above (reverse direction)
enabled
(boolean)
paused
(boolean)
⚠️ inverted sense on reads too —
enabled:true
≈
paused:false
status
(
live
/
learning
/
paused
, ROAS)
—🚫 not on the new response. Surface if the client branches on it.
scheduleStart
/
scheduleEnd
start
/
end
date → datetime;
end
nullable
biddingStrategy
(
manual
/
automated
)
bidConfigurationMethod
(
manual
/
auto
)
renamed + value change (
automated
→
auto
)
cpiBids[]
/
sourceBids[]
/
roasBids[]
/
eventOptimizationBids[]
manualBids[]
/
manualSourceBids[]
/
roasBids[]
/
appEventBids[]
renamed; only present with
includeFields=bids
; values in microdollars
retentionBids[]
—🚫 retention bids deprecated — no write support; treat any read value as legacy-only
budget
budget
only with
includeFields=budget
; microdollars +
campaignLimitType
/
dailyLimitType
eventOptimizationType
/
sdkEventName
appEventType
/
sdkEventNames[]
enum rename; single → array
autoStart
(
enabled
/
disabled
/
canceled
)
autoStart
(
ENABLED
/
DISABLED
/
CANCELED_REJECTED
/
CANCELED_LESS_THAN_TWO_PACK
)
uppercased;
canceled
split into two reasons
createdAt
/
updatedAt
createdAt
/
updatedAt
unchanged
—
applicationId
,
attribution
,
deviceTargeting
,
budgetPacingType
,
forcePausedEnabled
,
isAutoEligible
, …
new — see the new spec

Reads via
includeFields

Legacy read of a campaign's bids/budget/assigned packs used separate GETs. Now:
GET …/campaigns/{campaignId}?includeFields=bids,budget,creativePacks,application
. Bid arrays,
budget
,
creativePackIds[]
, and full
application
come back on the campaign object.

Block E — Bids (five endpoints → one)

What changed

The five legacy sub-resources (
cpi-bids
,
source-bids
,
roas-bids
,
retention-bids
,
event-optimization-bids
), each with
GET
/
PUT
/
PATCH
over dollar strings, collapse into a single
PATCH …/campaigns/{campaignId}/bids
over a typed body in microdollars. There is no GET — read current bids via
GET campaign?includeFields=bids
.

Endpoint + body remap

Legacy sub-resource

New body field in
UpdateCampaignPublicBidDto

Per-entry shape (new)

cpi-bids
manualBids[]
{ country, bid }
(
bid
in micros)
source-bids
manualSourceBids[]
{ country, sourceAppId, bid }
(micros)
roas-bids
roasBids[]
{ country, target, maxBid? }
(
maxBid
micros)
event-optimization-bids
appEventBids[]
{ country, bid }
(micros)
retention-bids
🚫 deprecated — no supportlegacy
{country, baseBid, maxBid}
has no successor (confirmed). Do not migrate; surface any client retention-bid usage to the human.

Money transform (critical)

Legacy dollar string → new microdollars: multiply by 1_000_000.
"0.201"
→
201000
. ROAS
goal
/
target
(confirmed): legacy ROAS
goal
was a percentage string (
"12.34"
= 12.34%); the new
target
is a decimal ratio — divide by 100 (
"12.34"
→
0.1234
; 10% →
0.10
). Semantics (confirmed by API owners):

Legacy PUT/PATCH model

New model

PUT
replaces the whole set;
PATCH
merges
The single
PATCH
replaces the full bid set of the given type — there is no merge. Always send the complete desired set.
Clearing bids (confirmed): send an empty array
[]
for that bid type in the
PATCH
— same effect as the legacy
PUT []
.
⚠️ Because the new
PATCH
replaces rather than merges, a naive port of legacy
PATCH
(merge) calls will silently drop every bid not included in the request. Convert legacy merge-style updates to full-set writes: read current bids via
GET campaign?includeFields=bids
, apply the delta locally, and send the complete array.

Block F — Budget → Budget Cap

What changed

  • GET …/budget
    is gone → read via
    GET campaign?includeFields=budget
    .
  • PATCH …/budget
    →
    PATCH …/campaigns/{campaignId}/budget-cap
    .
  • Dollars → microdollars (strings).
  • The per-country map (
    dailyPerCountry: { "US": "123.45", … }
    ) is replaced by named country groups (
    countryGroupDailyLimits: [{ name, countries[], limit }]
    ).
  • New explicit type discriminators on the response:
    campaignLimitType
    (
    unlimited
    /
    total
    ),
    dailyLimitType
    (
    daily
    /
    countryDaily
    /
    unlimited
    ).

Field mapping (
budget
→
PatchBudgetCapDto
)

Legacy field

New field

Notes

total
(dollars string)
total
(micros string)
×1e6. Cap representation semantics carry over from legacy (per API owners):
"0"
still means "no cap".
daily
(dollars string)
daily
(micros string)
×1e6
dailyPerCountry
(map)
countryGroupDailyLimits[]
Restructure: each
{country: amount}
becomes a group
{ name, countries:[country], limit: amount×1e6 }
. Group as the client prefers;
name
is required.
dailySpent
/
spent
(read-only)
— (read via campaign budget)present on the campaign budget read, not the PATCH response

Block G — Targeting

What changed

  • GET …/targeting
    is gone → read targeting fields off
    GET campaign
    .
  • PATCH …/targeting
    remains but the body is restructured and most legacy device fields are dropped (they were already
    deprecated
    in the legacy spec).

Field mapping (
targetingOptions
→
UpdateCampaignPublicTargetingDto
)

Legacy field

New field

Notes

appTargeting.allowList
/
blockList
allowlistSources[]
/
blocklistSources[]
renamed (source app ids)
categoryTargeting.allowList
/
blockList
apptopiaSourceCategoriesAllowlist/Blocklist[]
(+
sensorTower…
)
now provider-specific category lists
deviceTargeting
iOS
blockedDevices
/
osMin
appleDevice.deviceBlocklist[]
/
appleDevice.osMinVersion
restructured
deviceTargeting
Android
osMin
+ manufacturer/model
androidDevice.{manufacturerAllowlist,manufacturerBlocklist,modelBlocklist,osMinVersion}
restructured
regionalTargeting
allowlistCountrySubdivisions[] {country, subdivisions[]}
now subdivision-id based
limitedAdTracking
,
connectionType
,
screenSize
,
screenDensity
,
allowedDevices
,
osMax
—🚫 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:
    • PATCH …/campaigns/{campaignId}/assign-creative-pack
      with
      { creativePackIds: [...] }
      — replaces the assigned set with the provided ids.
    • DELETE …/campaigns/{campaignId}/unassign-creative-pack
      with
      { creativePackIds: [...] }
      — removes the listed ids.
  • List of currently-assigned packs:
    GET campaign?includeFields=creativePacks
    (
    creativePackIds[]
    ).

Migration approach

Collect all packs the client assigned to a campaign, then make one
PATCH assign-creative-pack
with the full array (instead of N
POST
s). Semantics confirmed: the endpoint replaces the complete assignment with the provided
creativePackIds
— always send the full desired set, or packs omitted from the array are unassigned.

Block I — Audience Pinpointer → Campaign Management

What changed

Legacy

New

GET …/audience-pinpointer/sdk-event-names
GET …/applications/{id}/sdk-event-names
GET …/audience-pinpointer/event-optimization-info
GET …/applications/{id}/eligibility
GET …/audience-pinpointer/roas-info
🚫 removed — ROAS campaigns no longer require eligibility; delete any gating logic
GET …/audience-pinpointer/retention-info
🚫 removed — retention is deprecated entirely; surface active retention usage
sdk-event-names
response:
{ total, results[]{ eventOptimizationType, sdkEventNames[] } }
.
eligibility
response:
{ total, results[]{ eventOptimizationType, countries[] } }
.

Block J — Attribution (relocated)

What changed

Attribution URLs were previously set on the app (
appAttributionClickUrl
/
StartUrl
) and passed into campaign create. The new API has a dedicated app-level attribution resource plus a per-campaign
attributionLinks
on campaign update.
  • GET …/applications/{applicationId}/attribution
    →
    { id, startUrl, clickUrl, viewUrl?, partner, type?, createdAt, updatedAt }
    .
  • PATCH …/applications/{applicationId}/attribution
    with
    { startUrl?, clickUrl?, viewUrl? }
    .
partner
enum:
other
,
adjust
,
airbridge
,
appsflyer
,
branch
,
bytebrew
,
kochava
,
mailru
,
singular
,
tenjin
,
yandex
.
type
enum:
userLevel
,
nonUserLevel
,
mixed
.

Migration approach

  1. Move app-level
    appAttributionClickUrl
    /
    appAttributionStartUrl
    →
    PATCH …/attribution
    (
    clickUrl
    /
    startUrl
    ).
  2. Move campaign-create
    attributionClickUrl
    /
    attributionStartUrl
    → a
    PATCH
    on the campaign (
    attributionLinks.{clickUrl,startUrl,viewUrl}
    ) after creating the campaign.
  3. viewUrl
    is new (optional).
    {ifa}
    and 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.

Suggested migration algorithm (deterministic order for the agent)

  1. Config: swap host to
    …/ads/advertiser
    , path prefix
    /advertise/v1
    →
    /public/v1
    , and the
    apps
    segment →
    applications
    . Keep Basic auth.
  2. Apps (Block A): for each legacy app,
    POST …/applications
    (remap
    store
    →
    {store, platform}
    ). Persist
    legacy campaignSetId → new applicationId
    . Do not send attribution URLs here.
  3. Attribution (Block J):
    PATCH …/applications/{id}/attribution
    with the old app-level URLs.
  4. Creatives (Block B): re-upload/re-map, translating the
    type
    enum. Persist
    legacy creativeId → new creativeId
    if ids are re-minted (verify whether ids are preserved).
  5. Creative Packs (Block C): create packs referencing new creative ids.
  6. Campaign Management (Block I): re-point audience-pinpointer lookups —
    …/audience-pinpointer/sdk-event-names
    →
    …/applications/{id}/sdk-event-names
    , and
    …/audience-pinpointer/event-optimization-info
    →
    …/applications/{id}/eligibility
    . Read-only; use the results to inform campaign
    appEventType
    /
    sdkEventNames
    and 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).
  7. Campaigns (Block D): create with
    optimizationType
    (+
    isCreativeTesting
    /
    appEventType
    /
    roasType
    ),
    start
    only. Then
    PATCH
    to set
    end
    ,
    attributionLinks
    ,
    autoStart
    ,
    paused
    .
  8. Bids (Block E): convert dollars → microdollars (ROAS targets: percent → decimal ratio, ÷100); send one
    PATCH …/bids
    with the complete desired set per bid type (the PATCH replaces, never merges;
    []
    clears). Retention bids are deprecated — do not migrate them; surface any usage.
  9. Budget (Block F):
    PATCH …/budget-cap
    in microdollars; convert
    dailyPerCountry
    map →
    countryGroupDailyLimits[]
    .
  10. Targeting (Block G): restructure into
    appleDevice
    /
    androidDevice
    /source+category lists; drop removed device fields (surface them).
  11. Assignment (Block H): one list-based
    PATCH assign-creative-pack
    per campaign.
  12. Reads: replace all
    GET …/{bids,budget,targeting,assigned-creative-packs}
    with
    GET campaign?includeFields=…
    .
  13. Removed features (surface to human):
    DELETE app
    (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,
    adomain
    , and
    squareVideo
    creatives.

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
    ,
    /assigned-creative-packs
    , or
    …/budget
    (now
    budget-cap
    ).
  • Every app path uses the new UUID
    applicationId
    , not the 24-char hex
    campaignSetId
    .
  • All money values are microdollars (integers), never dollar decimals like
    "2500.20"
    .
  • Bids go to the single
    PATCH …/bids
    ; reads use
    GET campaign?includeFields=bids
    .
  • Every bid
    PATCH
    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.
  • ROAS targets were converted from percent strings to decimal ratios (
    "12.34"
    →
    0.1234
    ).
  • Budget uses
    budget-cap
    +
    countryGroupDailyLimits[]
    (no
    dailyPerCountry
    map).
  • Campaign
    goal
    was remapped to
    optimizationType
    (+
    isCreativeTesting
    ), and attribution URLs are set via
    attributionLinks
    /
    …/attribution
    , not campaign create.
  • Creative
    type
    uses the new snake_case values (
    end_card_square
    ,
    video_portrait
    , …).
  • Every read of legacy
    enabled
    was remapped to
    paused
    with the sense inverted (
    enabled:true
    ⇒
    paused:false
    ) — on list, get, and update responses alike.
  • Nothing parses full campaign fields off the campaign
    PATCH
    response (it returns the slim details object); full state is re-fetched via
    GET campaign?includeFields=…
    .
  • Application list parsing expects a bare array (no
    {total, results}
    envelope) and no longer sends
    offset
    /
    limit
    /
    filter[store]
    /
    filter[storeId]
    .
  • Assignment uses one list-based call per campaign, not N single assigns.
  • DELETE app
    , deprecated retention usage (bids/eligibility),
    squareVideo
    creatives, and any removed targeting fields were surfaced to the human as gaps (if the client used them); ROAS eligibility gating was deleted.

Quick reference: 🚫 removed / no-equivalent (do not migrate or fabricate — all confirmed by API owners)

  • DELETE app
    — no delete-application endpoint; app lifecycle is owned by USF and deletes are currently disabled platform-wide.
  • ROAS-info — removed; ROAS campaigns no longer require eligibility. Delete gating logic.
  • Retention (whole product area) — deprecated:
    retention-info
    and retention bids have no successor. Surface active retention campaigns/bids.
  • GET bids / budget / targeting / assigned-packs — replaced by
    GET campaign?includeFields=…
    .
  • Legacy deprecated targeting fields (
    limitedAdTracking
    ,
    connectionType
    ,
    screenSize
    ,
    screenDensity
    ,
    allowedDevices
    ,
    osMax
    ) and app
    adomain
    — cannot be configured on unified; delete the client code.
  • squareVideo
    creative type
    — not supported; only
    video_portrait
    /
    video_landscape
    .