# 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.

> **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

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

```xml
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.                                                                               |
| 2     | **Money → microdollars**         | All bids/budgets were dollar strings (`"0.201"`, `"2500.20"`). New API is integer **microdollars** (1 USD = 1\_000\_000). Multiply by 1e6.                                                                                                               |
| 3     | **5 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`.                                                                               |
| 4     | **Sub-resource reads folded in** | Legacy `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`).                                                                                       |
| 6     | **Attribution relocated**        | Legacy 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**. |
| 7     | **Assignment is list-based**     | Legacy 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`                                                                                                                                                          |
| **Auth**                           | HTTP **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**                                                                                                                                                                                                                         |
| **Money**                          | dollar decimal strings (`"2500.20"`, bid `"0.201"`)                   | **microdollars** as integer strings/numbers (`"2500200000"`); 1 USD = 1\_000\_000                                                                                                                                                                  |
| **Campaign / creative / pack ids** | 24-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-resource | **20/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 endpoint                                | A         |
| `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 supported               | E         |
| `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 deprecated        | I         |
| — (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` | same                                                                       | unchanged               |
| `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 support**                     | legacy `{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`.
