# Publisher Public API migration reference for LLM coding agents

> A reference for LLM coding agents migrating a client's integration from the Monetize Manage API (legacy) to the Ads Unified Platform public API.

> **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" (`/monetize/v1/...`) to the **new** Unity Ads Publisher Public API
> (`/ads/publisher/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 =
   `ads-selfserve/src/servers/http-internal/openapi/public-v1.yaml`. New =
   `ads-unified-platform/openapi/publisher/platform-public-v1-routes.yaml`.
2. 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.
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 example
   is **eCPM Targets**, which do not exist in the new API.
4. 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)

```xml
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)

```xml
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 `apple` and a `google` store becomes **two applications** (one per platform). The "project" still exists as an optional grouping (`projectId`/`projectName` on app create) but is no longer the resource you operate on. |
| 2     | **Ad-Units removed**       | There is no ad-unit resource. Ad-unit-level settings (skip timer, mute, banner refresh) moved onto the **placement** as `adFormatConfigurations`. `adFormat` is now a **placement** property.                                                                    |
| 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 (`POST`/`PUT`/`DELETE` a single placement).                                                                                                        |

## Global changes (apply to all endpoints)

| **Aspect**                     | **Legacy**                                     | **New**                                                                                                                                     |
| ------------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| **Host**                       | `https://services.api.unity.com`               | `https://services.api.unity.com/ads/publisher` (prod) · `https://staging.services.api.unity.com/ads/publisher` (staging)                    |
| **Path prefix**                | `/monetize/v1`                                 | `/public/v1`                                                                                                                                |
| **Full example**               | `…/monetize/v1/organizations/{orgId}/projects` | `…/ads/publisher/public/v1/organizations/{organizationId}/applications`                                                                     |
| **Auth**                       | HTTP **Basic** (service-account key)           | HTTP **Basic** still works; Bearer JWT (`unity`/`ads` issuers) also accepted. **No change required** — keep your existing Basic credential. |
| **Org id**                     | `organizationId` (numeric string) in path      | `organizationId` (Genesis id, numeric) in path — **unchanged**                                                                              |
| **Primary id in path**         | `projectId` (uuid **or** numeric)              | `applicationId` (Unity App id, string)                                                                                                      |
| **`dryrun` query param**       | supported on writes                            | 🚫 removed                                                                                                                                  |
| **`showarchived` query param** | supported on reads                             | replaced by `isArchived` (boolean) on the placement list only                                                                               |
| **Archiving**                  | `archive: true/false` field in a PATCH body    | dedicated `DELETE` (archive) + `PATCH …/restore` (un-archive)                                                                               |

### Rate limits changed (update any client-side backoff assumptions)

|                                  | **Legacy**          | **New**              |
| -------------------------------- | ------------------- | -------------------- |
| Per-IP ceiling                   | 5/s, 4000 / 30 min  | **40/s**             |
| Reads (`GET`)                    | (covered by global) | **20/s + 8000/hour** |
| Creates (`POST`)                 | 5/s, 1000 / 30 min  | **1/s + 60/hour**    |
| Updates (`PATCH`/`PUT`/`DELETE`) | 5/s, 1000 / 30 min  | **1/s + 200/hour**   |

## Master endpoint map

| **Legacy operation**                                                                                                   | **Legacy route**                                                   | **New operation**                                                  | **New route**                                                                   | **Block** |
| ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------------------- | --------- |
| `getProjects`                                                                                                          | `GET /monetize/v1/organizations/{orgId}/projects`                  | `publicOrganizationApplications`                                   | `GET /public/v1/organizations/{orgId}/applications`                             | A         |
| `postProjects`                                                                                                         | `POST /monetize/v1/organizations/{orgId}/projects`                 | `publicCreateApplication`                                          | `POST /public/v1/organizations/{orgId}/applications`                            | A         |
| `getProjectDetails`                                                                                                    | `GET /monetize/v1/projects/{projectId}`                            | `publicGetApplicationById`                                         | `GET /public/v1/organizations/{orgId}/applications/{applicationId}`             | A         |
| `updateProjectDetails`                                                                                                 | `PATCH /monetize/v1/projects/{projectId}`                          | `publicUpdateApplication`                                          | `PATCH /public/v1/organizations/{orgId}/applications/{applicationId}`           | A         |
| `getAdUnitsByProject` / `getAdUnitsByProjectStore` / `createAdUnits` / `updateAdUnits` / `getAdUnits` / `patchAdUnits` | `…/projects/{projectId}/[stores/{storeName}/]adunits[/{adUnitId}]` | —                                                                  | 🚫 no ad-unit resource                                                          | B         |
| `createPlacements` (batch)                                                                                             | `POST …/adunits/{adUnitId}/placements`                             | `publicCreatePlacement` (single)                                   | `POST /public/v1/organizations/{orgId}/applications/{applicationId}/placements` | C         |
| `GetPlacements` (by ad-unit)                                                                                           | `GET …/adunits/{adUnitId}/placements`                              | `publicApplicationPlacements`                                      | `GET …/applications/{applicationId}/placements`                                 | C         |
| `getPlacement`                                                                                                         | `GET …/placements/{placementId}`                                   | `publicGetPlacementDetails`                                        | `GET …/applications/{applicationId}/placements/{placementId}`                   | C         |
| `updatePlacements` (batch) / `updatePlacement`                                                                         | `PATCH …/placements[/{placementId}]`                               | `publicUpdatePlacement` (single, **PUT**)                          | `PUT …/applications/{applicationId}/placements/{placementId}`                   | C         |
| (archive via `archive:true`)                                                                                           | `PATCH …/placements/{placementId}`                                 | `publicArchivePlacement`                                           | `DELETE …/applications/{applicationId}/placements/{placementId}`                | C         |
| (un-archive via `archive:false`)                                                                                       | `PATCH …/placements/{placementId}`                                 | `publicUnArchivePlacement`                                         | `PATCH …/applications/{applicationId}/placements/{placementId}/restore`         | C         |
| `GetPlacements` (org)                                                                                                  | `GET /monetize/v1/organizations/{orgId}/placements`                | `publicOrganizationPlacements`                                     | `GET /public/v1/organizations/{orgId}/placements`                               | D         |
| `getTargets` / `createTargets` / `getTargetDetail` / `patchTarget` / `deleteTarget`                                    | `…/placements/{placementId}/targets[/{targetId}]`                  | —                                                                  | 🚫 eCPM targets removed                                                         | E         |
| —                                                                                                                      | —                                                                  | `publicGetApplicationTestMode` / `publicUpdateApplicationTestMode` | `GET`/`PATCH …/applications/{applicationId}/test-mode`                          | F (new)   |
| —                                                                                                                      | —                                                                  | test-device CRUD                                                   | `…/organizations/{orgId}/test-devices[/{testDeviceId}]`                         | 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 (`/organizations/{orgId}/applications/{applicationId}`)
  and keyed by **`applicationId`** (Unity App id), not the standalone `/projects/{projectId}`.
* A legacy project carried **both** stores; a new application is **single-platform**. Split
  accordingly (see transform rules).
* `adsProvider` is gone. `coppa` changed from an enum string to a boolean, and moved under a
  `privacy` object alongside the new `mixedAudience` boolean.

### Endpoint mapping

| **Legacy**                              | **New**                                                      |
| --------------------------------------- | ------------------------------------------------------------ |
| `GET …/organizations/{orgId}/projects`  | `GET …/organizations/{orgId}/applications`                   |
| `POST …/organizations/{orgId}/projects` | `POST …/organizations/{orgId}/applications`                  |
| `GET …/projects/{projectId}`            | `GET …/organizations/{orgId}/applications/{applicationId}`   |
| `PATCH …/projects/{projectId}`          | `PATCH …/organizations/{orgId}/applications/{applicationId}` |

### Field mapping — **create** (`createProject` → `CreateApplicationPublicDto`)

| **Legacy field**                                         | **New field**                                  | **Notes**                                                                              |
| -------------------------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------- |
| `name` (required)                                        | `name` (required)                              | unchanged                                                                              |
| `adsProvider` (required, enum)                           | —                                              | 🚫 **Dropped.** Do not send. No equivalent.                                            |
| `coppa` (required, `"compliant"`/`"non_compliant"`)      | `privacy.coppa` (boolean)                      | Transform: `"compliant"` → `true`, `"non_compliant"` → `false`.                        |
| —                                                        | `privacy.mixedAudience` (boolean)              | **New.** Required inside `privacy`. Default `false` unless the client knows otherwise. |
| `stores.apple` / `stores.google` (`{storeId, storeUrl}`) | `store` (enum) + `storeId` + `platform`        | **Split into per-platform apps.** See transform rules.                                 |
| —                                                        | `platform` (required, enum: `iOS`,`Android`,…) | **New, required.** Derive from which store the app targets.                            |
| —                                                        | `iconUrl` (uri)                                | new, optional                                                                          |
| —                                                        | `projectId` (uuid)                             | optional — associate with an existing project                                          |
| —                                                        | `projectName` (string)                         | optional — create+associate a new project; defaults to app name                        |

### Field mapping — **update** (`updateProject` → `UpdateApplicationPublicDto`)

| **Legacy field** | **New field**             | **Notes**                                                            |
| ---------------- | ------------------------- | -------------------------------------------------------------------- |
| `adsProvider`    | —                         | 🚫 dropped                                                           |
| `coppa` (enum)   | `privacy.coppa` (boolean) | same transform as create; both `privacy.*` fields optional on update |
| `stores`         | `store` + `storeId`       | one platform per app                                                 |
| —                | `name`                    | now updatable                                                        |
| —                | `kidsSettings` (boolean)  | new; `true` triggers a downstream coppa=true update                  |

### Field mapping — **response** (`project`/`projectDetail` → `ApplicationPublicResponseDto`)

| **Legacy field**               | **New field**                                 | **Notes**                                                                                        |
| ------------------------------ | --------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `id` (project id)              | `id` (application id) + `projectId`           | The response now has an **application**`id` and a separate `projectId` for the grouping project. |
| `name`                         | `name`                                        | unchanged                                                                                        |
| `coppa` (enum)                 | `coppa` (boolean) + `mixedAudience` (boolean) | enum→boolean                                                                                     |
| `adsProvider`                  | —                                             | 🚫 gone                                                                                          |
| `stores` (map of apple/google) | `store` + `storeId` + `platform`              | single store                                                                                     |
| `archivedAt`                   | —                                             | not on the app response                                                                          |
| `maxCaps`                      | —                                             | 🚫 gone                                                                                          |
| —                              | `iconUrl`, `testMode`, `kidsSettings`         | new                                                                                              |

### Transform rule: one legacy project → N applications

For a legacy project with `stores.apple` **and** `stores.google`, emit **two** `POST /applications`
calls — one with `platform: iOS`, `store: AppleAppStore`, `storeId: <apple.storeId>`, and one with
`platform: Android`, `store: GooglePlay`, `storeId: <google.storeId>`. To keep them grouped under a
single project, create the first app with `projectName: <project.name>`, capture the returned
`projectId`, then create the second app with that `projectId`.

### Example

**Legacy — create project (both stores):**

```json
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:**

```json
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…` (`getAdUnitsByProject`,
`getAdUnitsByProjectStore`, `createAdUnits`, `updateAdUnits`, `getAdUnits`, `patchAdUnits`).

### Where ad-unit data went

| **Legacy ad-unit field**                        | **New home**                                                                     |
| ----------------------------------------------- | -------------------------------------------------------------------------------- |
| `adFormat` (`rewarded`/`interstitial`/`banner`) | **Placement**`adFormat` (now per placement).                                     |
| `allowSkipInSeconds`, `muteVideo`               | Placement `adFormatConfigurations.adminSettings.{allowSkipInSeconds, muteVideo}` |
| `bannerRefreshRate`                             | Placement `adFormatConfigurations.bannerRefreshRate` (banner only)               |
| `adTypes` (`display`/`playable`/`video`)        | 🚫 No direct equivalent. Surface to the human if the client relied on it.        |
| nested `placements` map                         | 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 `adFormat` onto the
placement and folding the ad-unit's admin settings into the placement's `adFormatConfigurations`.

***

## Block C — Placements (per-application)

### What changed

* Path is now `…/applications/{applicationId}/placements[/{placementId}]` — no store/ad-unit
  segments.
* **Individual** operations replace batch maps/arrays.
* **`adFormat` is required** on each placement (was an ad-unit property).
* New **`adFormatConfigurations`** object holds the player/admin settings; shape depends on
  `adFormat` (Rewarded/Interstitial/Banner).
* **eCPM `targets` are gone** from the placement body (see Block E).
* Identifiers: legacy `placementId` was a slug (`^[\da-zA-Z_-]+$`); the new placement `id` is a
  **UUID**, and the human-readable slug is returned separately as **`key`** (generated from `name`).
* Archive/restore are dedicated operations, not a body flag.
* Per-placement **update is `PUT`** (full replacement), not `PATCH`.

### Endpoint mapping

| **Intent** | **Legacy**                                           | **New**                                                                               |
| ---------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------- |
| Create     | `POST …/adunits/{adUnitId}/placements` (batch array) | `POST …/applications/{applicationId}/placements` (one)                                |
| List       | `GET …/adunits/{adUnitId}/placements`                | `GET …/applications/{applicationId}/placements` (filters: `isArchived`, `adFormat[]`) |
| Get one    | `GET …/placements/{placementId}`                     | `GET …/applications/{applicationId}/placements/{placementId}`                         |
| Update     | `PATCH …/placements/{placementId}` (or batch)        | `PUT …/applications/{applicationId}/placements/{placementId}`                         |
| Archive    | `PATCH …` with `{"archive": true}`                   | `DELETE …/placements/{placementId}` (204)                                             |
| Un-archive | `PATCH …` with `{"archive": false}`                  | `PATCH …/placements/{placementId}/restore`                                            |

### Field mapping — **request** (`addPlacements[]` / `patchPlacementObject` → `PlacementPublicRequestDto`)

| **Legacy field**                | **New field**                                                  | **Notes**                                                                    |
| ------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `name` (required)               | `name` (required)                                              | unchanged                                                                    |
| `id` (slug, on create)          | —                                                              | 🚫 You no longer choose the id. Server returns a UUID `id` and a `key` slug. |
| —                               | `adFormat` (required, enum `rewarded`/`interstitial`/`banner`) | **New & required.** Source from the legacy parent ad-unit's `adFormat`.      |
| `addTargets` / `targets` (eCPM) | —                                                              | 🚫 Removed. Do not send. See Block E.                                        |
| `placementType`                 | —                                                              | not accepted on write                                                        |
| `archive` (boolean)             | —                                                              | use `DELETE`/`restore` instead                                               |
| —                               | `adFormatConfigurations`                                       | **New.** Player/admin settings; see shape below.                             |

### `adFormatConfigurations` shape (by `adFormat`)

* **rewarded** (`RewardedConfigurations`): `name` (reward name, required), `value` (reward value,
  required), `adminSettings` (`AdminConfigurations`).
* **interstitial** (`InterstitialConfigurations`): `adminSettings`.
* **banner** (`BannerConfigurations`): `adminSettings`, `bannerRefreshRate` (required).
* **`AdminConfigurations`**: `allowSkip`, `allowSkipInSeconds`, `videoPlayableSkipInSeconds`,
  `closeTimerDuration`, `tapsToClose`, `muteVideo`, `disableVideoControlsFade`,
  `useCloseIconInsteadOfSkipIcon`.

### Field mapping — **response** (`placement` → `PlacementPublicResponseDto`)

| **Legacy field** | **New field**                                                                                                 | **Notes**                                                    |
| ---------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `id` (slug)      | `key` (slug)                                                                                                  | The legacy slug-style id maps to the new **`key`**.          |
| —                | `id` (uuid)                                                                                                   | **New** canonical id; this is what goes in the URL path.     |
| `name`           | `name`                                                                                                        | unchanged                                                    |
| `archivedAt`     | `archivedAt` (date-time, nullable)                                                                            | now ISO-8601; `null` when active                             |
| `placementType`  | —                                                                                                             | not on the per-app response (it is on the org list, Block D) |
| `targets`        | —                                                                                                             | 🚫 removed                                                   |
| —                | `adFormat`, `status` (`active`/`paused`), `adFormatConfigurations`, `applicationId`, `createdAt`, `updatedAt` | 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 UUID `id` for path operations, while the slug
> survives as `key`. Build and persist a `legacy placement slug → new {id, key}` lookup during
> migration.

### Example

**Legacy — batch create under an ad-unit:**

```json
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):**

```json
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 `targets` are
gone; `adFormat`, `platform`, and `storeId` are added; the legacy `id` becomes `placementId`
(the human-readable key). Only **active iOS/Android** placements are returned.

### Endpoint mapping

| **Legacy**                                          | **New**                                           |
| --------------------------------------------------- | ------------------------------------------------- |
| `GET /monetize/v1/organizations/{orgId}/placements` | `GET /public/v1/organizations/{orgId}/placements` |

### Field mapping (`placementWithGameId` → `OrganizationPlacementPublicResponseDto`)

| **Legacy field**  | **New field**                                            | **Notes**                                          |
| ----------------- | -------------------------------------------------------- | -------------------------------------------------- |
| `id`              | `placementId`                                            | now the human-readable key (e.g. `Banner_Android`) |
| `name`            | `name`                                                   | unchanged                                          |
| `placementType`   | `placementType`                                          | unchanged (string, e.g. `bidding`)                 |
| `gameId`          | `gameId` (integer, nullable)                             | unchanged                                          |
| `targets` (array) | —                                                        | 🚫 removed                                         |
| —                 | `adFormat` (`rewarded`/`interstitial`/`banner`/`native`) | new                                                |
| —                 | `platform` (`ios`/`android`/`""`)                        | new; `""` when neither iOS nor Android             |
| —                 | `storeId` (nullable)                                     | 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:
`getTargets`, `createTargets`, `getTargetDetail`, `patchTarget`, `deleteTarget`, plus the
`targets`/`addTargets` fields on placements/ad-units.

**Agent instructions:**

1. Do **not** map these to any new endpoint. Do not fabricate `…/targets` paths.
2. Remove `targets`/`addTargets` from all placement create/update payloads.
3. 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 }`
* `PATCH …/applications/{applicationId}/test-mode` with `{ "testMode": "forceAll" | "forceOff" }`

`testMode` is also present on the application response (Block A). Migration is additive only —
there is nothing in the legacy API to port; expose these if the client wants test-mode control.

## Block G — Test Devices (NEW)

No legacy equivalent. Org-scoped CRUD:

* `POST …/organizations/{orgId}/test-devices` — `CreateTestDevicePublicDto` `{ name*, advertisingId*, platform? }`
* `GET …/organizations/{orgId}/test-devices` — list (optional `platform` filter)
* `GET …/organizations/{orgId}/test-devices/{testDeviceId}` — get one
* `PATCH …/organizations/{orgId}/test-devices/{testDeviceId}` — `UpdateTestDevicePublicDto`
* `DELETE …/organizations/{orgId}/test-devices/{testDeviceId}` — 204

`testDeviceId` is a UUID. `platform` uses the full platform enum
(`Android`, `iOS`, `OSX`, `Windows`, `Linux`, `WebGL`, `Windows_Store`, `PS4`, `PS5`, `XboxOne`,
`tvOS`, `Switch`, `VisionOS`). Migration is additive only.

## Suggested migration algorithm (deterministic order for the agent)

1. **Config:** swap base host to `…/ads/publisher` and the path prefix `/monetize/v1` → `/public/v1`.
   Keep Basic auth. Remove all `dryrun` params.
2. **Applications (Block A):** for each legacy project, create one application per store; transform
   `coppa` enum→boolean, drop `adsProvider`, set `platform`/`store`/`storeId`. Group via
   `projectName`/`projectId`. Persist a `legacy projectId+store → new {applicationId, projectId}` map.
3. **Placements (Block C):** for each legacy placement, resolve the owning application from the map,
   create it individually with `adFormat` (from the legacy ad-unit) + `adFormatConfigurations`
   (from ad-unit/admin settings). **Drop `targets`/`addTargets`.** Persist
   `legacy placement slug → new {id, key}`.
4. **Re-point identifiers:** replace stored legacy placement slugs with the new UUID `id` for path
   ops (keep `key` for display). Replace stored `projectId`-based app refs with `applicationId`.
5. **Archive semantics:** replace `archive:true/false` PATCH calls with `DELETE` / `PATCH …/restore`.
6. **List/read shapes:** update response parsing per the field tables (esp. org list — Block D).
7. **eCPM Targets (Block E):** remove target code; **report the gap to the human**.
8. **Optional new features:** wire up Test Mode (F) and Test Devices (G) only if requested.
9. **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`, `/stores/`, or `/targets`.
* [ ] No request body contains `adsProvider`, `addTargets`, `targets`, or `archive`.
* [ ] `coppa` is a boolean everywhere (never `"compliant"`/`"non_compliant"`).
* [ ] Every placement create/update sends a valid `adFormat` and an `adFormatConfigurations`
  matching that format.
* [ ] Placement path params use the new UUID `id`, not the legacy slug (`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 `targets`/`addTargets` fields.
* Project `adsProvider` and `maxCaps`.
* `dryrun` and `showarchived` query params.
* Client-chosen placement ids (the server assigns the UUID; you keep the slug as `key`).
