Publisher Public API migration reference for LLM coding agents
A reference for LLM coding agents migrating a client's integration from the legacy Monetize Manage API to the new Unity Ads Publisher Public API v2.
Read time 10 minutesLast updated 20 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 "Monetize Manage API" () to the new Unity Ads Publisher Public API (/monetize/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./ads/publisher/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 =
ads-selfserve/src/servers/http-internal/openapi/public-v1.yaml.ads-unified-platform/openapi/publisher/platform-public-v1-routes.yaml - Migrate one entity block at a time, in the order given (Applications → Placements → org-level lists). Do not start a block until the one it depends on compiles/passes.
- 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 example is eCPM Targets, which do not exist in the new API.
- After each block, re-map field names per the field tables; never assume a field carried over unchanged unless the table says "unchanged".
Conceptual model change (read this first)
The data model was flattened and re-centered from Project to Application.
Legacy model (nested, project-centric)
Organization└── Project (spans BOTH stores; has coppa + adsProvider) └── Store (apple | google) (in the URL path) └── Ad-Unit (adFormat, skip/mute/banner settings live here) └── Placement └── eCPM Target (geo-based floor prices)
New model (flat, application-centric)
Organization└── Application (ONE platform + ONE store each; has privacy + testMode) └── Placement (adFormat + adFormatConfigurations live here; no targets)Organization└── Test Device (NEW, org-scoped)
The four shifts that drive every transformation
# | Shift | Consequence for migration |
|---|---|---|
| 1 | Project → Application | A legacy project that had both an |
| 2 | Ad-Units removed | There is no ad-unit resource. Ad-unit-level settings (skip timer, mute, banner refresh) moved onto the placement as |
| 3 | eCPM Targets removed | 🚫 No equivalent in the new public API. |
| 4 | Batch → individual ops | Legacy created/updated placements and ad-units in bulk (maps/arrays). The new API is one resource per request ( |
Global changes (apply to all endpoints)
Aspect | Legacy | New |
|---|---|---|
| Host | | |
| Path prefix | | |
| Full example | | |
| Auth | HTTP Basic (service-account key) | HTTP Basic still works; Bearer JWT ( |
| Org id | | |
| Primary id in path | | |
| supported on writes | 🚫 removed |
| supported on reads | replaced by |
| Archiving | | dedicated |
Rate limits changed (update any client-side backoff assumptions)
Legacy | New | |
|---|---|---|
| Per-IP ceiling | 5/s, 4000 / 30 min | 40/s |
Reads ( | (covered by global) | 20/s + 8000/hour |
Creates ( | 5/s, 1000 / 30 min | 1/s + 60/hour |
Updates ( | 5/s, 1000 / 30 min | 1/s + 200/hour |
Master endpoint map
Legacy operation | Legacy route | New operation | New route | Block |
|---|---|---|---|---|
| | | | A |
| | | | A |
| | | | A |
| | | | A |
| | — | 🚫 no ad-unit resource | B |
| | | | C |
| | | | C |
| | | | C |
| | | | C |
(archive via | | | | C |
(un-archive via | | | | C |
| | | | D |
| | — | 🚫 eCPM targets removed | E |
| — | — | | | F (new) |
| — | — | test-device CRUD | | G (new) |
Block A - Projects → Applications
What changed
- The resource you list/create/read/update is now the Application, not the Project.
- Read/update are now nested under the org () and keyed by
/organizations/{orgId}/applications/{applicationId}(Unity App id), not the standaloneapplicationId./projects/{projectId} - A legacy project carried both stores; a new application is single-platform. Split accordingly (see transform rules).
- is gone.
adsProviderchanged from an enum string to a boolean, and moved under acoppaobject alongside the newprivacyboolean.mixedAudience
Endpoint mapping
Legacy | New |
|---|---|
| |
| |
| |
| |
Field mapping — create (createProject
→ CreateApplicationPublicDto
)
createProjectCreateApplicationPublicDtoLegacy field | New field | Notes |
|---|---|---|
| | unchanged |
| — | 🚫 Dropped. Do not send. No equivalent. |
| | Transform: |
| — | | New. Required inside |
| | Split into per-platform apps. See transform rules. |
| — | | New, required. Derive from which store the app targets. |
| — | | new, optional |
| — | | optional — associate with an existing project |
| — | | optional — create+associate a new project; defaults to app name |
Field mapping — update (updateProject
→ UpdateApplicationPublicDto
)
updateProjectUpdateApplicationPublicDtoLegacy field | New field | Notes |
|---|---|---|
| — | 🚫 dropped |
| | same transform as create; both |
| | one platform per app |
| — | | now updatable |
| — | | new; |
Field mapping — response (project
/projectDetail
→ ApplicationPublicResponseDto
)
projectprojectDetailApplicationPublicResponseDtoLegacy field | New field | Notes |
|---|---|---|
| | The response now has an application |
| | unchanged |
| | enum→boolean |
| — | 🚫 gone |
| | single store |
| — | not on the app response |
| — | 🚫 gone |
| — | | new |
Transform rule: one legacy project → N applications
For a legacy project with and , emit two
calls — one with , , , and one with
, , . To keep them grouped under a
single project, create the first app with , capture the returned
, then create the second app with that .
stores.applestores.googlePOST /applicationsplatform: iOSstore: AppleAppStorestoreId: <apple.storeId>platform: Androidstore: GooglePlaystoreId: <google.storeId>projectName: <project.name>projectIdprojectIdExample
Legacy — create project (both stores):
POST /monetize/v1/organizations/3573617062594/projects{ "name": "My Game", "adsProvider": "unity", "coppa": "non_compliant", "stores": { "apple": { "storeId": "1479198816", "storeUrl": "https://apps.apple.com/..." }, "google": { "storeId": "com.unity.trashdash", "storeUrl": "https://play.google.com/..." } }}
New — two application creates, grouped under one project:
POST /ads/publisher/public/v1/organizations/3573617062594/applications{ "name": "My Game", "platform": "iOS", "store": "AppleAppStore", "storeId": "1479198816", "privacy": { "coppa": false, "mixedAudience": false }, "projectName": "My Game"}// → response.projectId = "5a8591dd-..."; reuse it for the second app:POST /ads/publisher/public/v1/organizations/3573617062594/applications{ "name": "My Game", "platform": "Android", "store": "GooglePlay", "storeId": "com.unity.trashdash", "privacy": { "coppa": false, "mixedAudience": false }, "projectId": "5a8591dd-..."}
Block B — Ad-Units → removed (folded into Application + Placement)
🚫 No equivalent resource
There are no ad-unit endpoints. Delete all client code that calls
(,
, , , , ).
…/projects/{projectId}/[stores/{storeName}/]adunits…getAdUnitsByProjectgetAdUnitsByProjectStorecreateAdUnitsupdateAdUnitsgetAdUnitspatchAdUnitsWhere ad-unit data went
Legacy ad-unit field | New home |
|---|---|
| Placement |
| Placement |
| Placement |
| 🚫 No direct equivalent. Surface to the human if the client relied on it. |
nested | Create placements individually under the application (Block C). |
Migration approach: treat each legacy ad-unit as a grouping of placements with a shared
adFormat. For every placement that lived under a legacy ad-unit, create a new placement under the
application (the one matching that ad-unit's store), copying the ad-unit's onto the
placement and folding the ad-unit's admin settings into the placement's .
adFormatadFormatConfigurationsBlock C — Placements (per-application)
What changed
- Path is now — no store/ad-unit segments.
…/applications/{applicationId}/placements[/{placementId}] - Individual operations replace batch maps/arrays.
- is required on each placement (was an ad-unit property).
adFormat - New object holds the player/admin settings; shape depends on
adFormatConfigurations(Rewarded/Interstitial/Banner).adFormat - eCPM are gone from the placement body (see Block E).
targets - Identifiers: legacy was a slug (
placementId); the new placement^[\da-zA-Z_-]+$is a UUID, and the human-readable slug is returned separately asid(generated fromkey).name - Archive/restore are dedicated operations, not a body flag.
- Per-placement update is (full replacement), not
PUT.PATCH
Endpoint mapping
Intent | Legacy | New |
|---|---|---|
| Create | | |
| List | | |
| Get one | | |
| Update | | |
| Archive | | |
| Un-archive | | |
Field mapping — request (addPlacements[]
/ patchPlacementObject
→ PlacementPublicRequestDto
)
addPlacements[]patchPlacementObjectPlacementPublicRequestDtoLegacy field | New field | Notes |
|---|---|---|
| | unchanged |
| — | 🚫 You no longer choose the id. Server returns a UUID |
| — | | New & required. Source from the legacy parent ad-unit's |
| — | 🚫 Removed. Do not send. See Block E. |
| — | not accepted on write |
| — | use |
| — | | New. Player/admin settings; see shape below. |
adFormatConfigurations
shape (by adFormat
)
adFormatConfigurationsadFormat- rewarded ():
RewardedConfigurations(reward name, required),name(reward value, required),value(adminSettings).AdminConfigurations - interstitial ():
InterstitialConfigurations.adminSettings - banner ():
BannerConfigurations,adminSettings(required).bannerRefreshRate - :
AdminConfigurations,allowSkip,allowSkipInSeconds,videoPlayableSkipInSeconds,closeTimerDuration,tapsToClose,muteVideo,disableVideoControlsFade.useCloseIconInsteadOfSkipIcon
Field mapping — response (placement
→ PlacementPublicResponseDto
)
placementPlacementPublicResponseDtoLegacy field | New field | Notes |
|---|---|---|
| | The legacy slug-style id maps to the new |
| — | | New canonical id; this is what goes in the URL path. |
| | unchanged |
| | now ISO-8601; |
| — | not on the per-app response (it is on the org list, Block D) |
| — | 🚫 removed |
| — | | new |
⚠️ ID remap is the highest-risk step. Anywhere the client persisted a legacy placement slug as the identifier, it must now store the new UUIDfor path operations, while the slug survives asid. Build and persist akeylookup during migration.legacy placement slug → new {id, key}
Example
Legacy — batch create under an ad-unit:
POST /monetize/v1/projects/{projectId}/stores/apple/adunits/{adUnitId}/placements[ { "name": "Rewarded Placement", "id": "rewarded_01", "addTargets": [ { "value": 18.5, "type": "global" } ] } ]
New — one create per placement (targets dropped, adFormat + config added):
POST /ads/publisher/public/v1/organizations/{orgId}/applications/{applicationId}/placements{ "name": "Rewarded Placement", "adFormat": "rewarded", "adFormatConfigurations": { "name": "coins", "value": 100, "adminSettings": { "allowSkip": true, "allowSkipInSeconds": 5, "muteVideo": false } }}
Block D — Organization placements list
What changed
Same intent (list all placements in the org) but a different response shape: eCPM are
gone; , , and are added; the legacy becomes
(the human-readable key). Only active iOS/Android placements are returned.
targetsadFormatplatformstoreIdidplacementIdEndpoint mapping
Legacy | New |
|---|---|
| |
Field mapping (placementWithGameId
→ OrganizationPlacementPublicResponseDto
)
placementWithGameIdOrganizationPlacementPublicResponseDtoLegacy field | New field | Notes |
|---|---|---|
| | now the human-readable key (e.g. |
| | unchanged |
| | unchanged (string, e.g. |
| | unchanged |
| — | 🚫 removed |
| — | | new |
| — | | new; |
| — | | new |
Block E - eCPM Targets Removed
🚫 No equivalent in the new public API
Every legacy target endpoint is gone and has no replacement in this API:
, , , , , plus the
/ fields on placements/ad-units.
getTargetscreateTargetsgetTargetDetailpatchTargetdeleteTargettargetsaddTargetsAgent instructions:
- Do not map these to any new endpoint. Do not fabricate paths.
…/targets - Remove /
targetsfrom all placement create/update payloads.addTargets - If the client's integration manages eCPM floor prices via this API, stop and report this to the human as a blocking gap — eCPM target management is not covered by the new Publisher Public API and must be handled out of band (e.g. via a different Unity surface or product decision). Flag it explicitly; do not silently drop pricing logic without surfacing it.
Block F — Application Test Mode (NEW)
No legacy equivalent. New endpoints:
- →
GET …/applications/{applicationId}/test-mode{ id, testMode } - with
PATCH …/applications/{applicationId}/test-mode{ "testMode": "forceAll" | "forceOff" }
testModeBlock G — Test Devices (NEW)
No legacy equivalent. Org-scoped CRUD:
- —
POST …/organizations/{orgId}/test-devicesCreateTestDevicePublicDto{ name*, advertisingId*, platform? } - — list (optional
GET …/organizations/{orgId}/test-devicesfilter)platform - — get one
GET …/organizations/{orgId}/test-devices/{testDeviceId} - —
PATCH …/organizations/{orgId}/test-devices/{testDeviceId}UpdateTestDevicePublicDto - — 204
DELETE …/organizations/{orgId}/test-devices/{testDeviceId}
testDeviceIdplatformAndroidiOSOSXWindowsLinuxWebGLWindows_StorePS4PS5XboxOnetvOSSwitchVisionOSSuggested migration algorithm (deterministic order for the agent)
- Config: swap base host to and the path prefix
…/ads/publisher→/monetize/v1. Keep Basic auth. Remove all/public/v1params.dryrun - Applications (Block A): for each legacy project, create one application per store; transform
enum→boolean, drop
coppa, setadsProvider/platform/store. Group viastoreId/projectName. Persist aprojectIdmap.legacy projectId+store → new {applicationId, projectId} - Placements (Block C): for each legacy placement, resolve the owning application from the map,
create it individually with (from the legacy ad-unit) +
adFormat(from ad-unit/admin settings). DropadFormatConfigurations/targets. PersistaddTargets.legacy placement slug → new {id, key} - Re-point identifiers: replace stored legacy placement slugs with the new UUID for path ops (keep
idfor display). Replace storedkey-based app refs withprojectId.applicationId - Archive semantics: replace PATCH calls with
archive:true/false/DELETE.PATCH …/restore - List/read shapes: update response parsing per the field tables (esp. org list — Block D).
- eCPM Targets (Block E): remove target code; report the gap to the human.
- Optional new features: wire up Test Mode (F) and Test Devices (G) only if requested.
- Rate limits: cap creates at ≤1/s and ≤60/hour; reads ≤20/s.
Validation checklist (run after migration)
- No request path contains ,
/monetize/v1,/projects/{,/adunits, or/stores/./targets - No request body contains ,
adsProvider,addTargets, ortargets.archive - is a boolean everywhere (never
coppa/"compliant")."non_compliant" - Every placement create/update sends a valid and an
adFormatmatching that format.adFormatConfigurations - Placement path params use the new UUID , not the legacy slug (
id).key - A legacy project with two stores produced two applications sharing one .
projectId - eCPM target management was surfaced to the human as an unsupported gap (if the client used it).
Quick reference: 🚫 removed / no-equivalent (do not migrate or fabricate)
- All Ad-Unit endpoints and the ad-unit resource.
- All eCPM Target endpoints and /
targetsfields.addTargets - Project and
adsProvider.maxCaps - and
dryrunquery params.showarchived - Client-chosen placement ids (the server assigns the UUID; you keep the slug as ).
key