Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/library-level-creative-merge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"adcontextprotocol": minor
---

Add a library-level `merge` to `sync_creatives` `assignment_operations` (adcp#8897). A `merge` without `package_id` declares that a seller-assigned creative is the buyer's creative everywhere in the account. The seller merges every current assignment of it, makes every later assignment under the buyer's `creative_id`, and keeps listing the seller-assigned creative in `list_creatives` with the new `merged_into_creative_id`, which gives buyers a durable record for joining delivery from before the merge. Gated by the new `creative.supports_library_merge` capability, which requires `supports_assignment_merge`; a seller without it rejects a `merge` without `package_id` with `UNSUPPORTED_FEATURE`. `merges[]` reports the account-wide binding as an entry without `package_id`, followed by one entry per moved assignment. `CREATIVE_MERGE_REJECTED` details may omit `package_id` and gain the `not_in_account` and `already_merged` reasons. Package merges are unchanged, and delivery before any merge is still not restated.
1 change: 1 addition & 0 deletions docs/creative/specification.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,7 @@ Upload and synchronize creative assets in a library. Implemented by any agent th
- Agents MUST reject requests that combine `delete_missing: true` with `creative_ids` — `delete_missing` applies to the entire library, not a filtered subset
- Agents that declare `creative.stable_creative_ids` MUST NOT archive a creative that entered the library outside `sync_creatives` under `delete_missing`. A sync of an existing `creative_id` updates that creative under the normal upsert rules, whatever created it. See [Seller-assigned creative IDs](/docs/creative/task-reference/sync_creatives#seller-assigned-creative-ids).
- Agents that advertise `creative.supports_assignment_merge` MUST apply a verified `merge` assignment operation by moving the seller-assigned creative's package assignment, with its serving object and approval, to the buyer's `creative_id` without re-review unless the content differs, and MUST report that object's delivery under the buyer's `creative_id` from the merge onward without restating earlier delivery. See [Merging a seller-created copy](/docs/creative/task-reference/sync_creatives#merging-a-seller-created-copy).
- Agents that advertise `creative.supports_library_merge` MUST apply a verified `merge` without `package_id` by merging every current assignment of the seller-assigned creative into the buyer's `creative_id`, making every later assignment of it under that `creative_id`, and listing the seller-assigned creative with `merged_into_creative_id`. Earlier delivery stays under the seller-assigned ID. See [Merging across the account](/docs/creative/task-reference/sync_creatives#merging-across-the-account).
- Agents that advertise `creative.localization` MUST reject unsupported localization before mutation and return exact localization readback on every accepted localized creative
- Agents that also manage media buys SHOULD support the `assignments` field for bulk creative-to-package mapping
- Standalone creative agents that do not manage media buys SHOULD ignore the `assignments` field
Expand Down
3 changes: 2 additions & 1 deletion docs/creative/task-reference/list_creatives.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ not maintain it omits it and the buyer falls back to rereading.
|-------|------|-------------|
| `creative_id` | string | Unique creative identifier. Assigned by the seller for creatives created outside AdCP. |
| `platform_id` | string | The seller's identifier for the creative in its source of truth. Same value `sync_creatives` returns. |
| `merged_into_creative_id` | string | Present on a seller-assigned creative that a [library-level merge](/docs/creative/task-reference/sync_creatives#merging-across-the-account) bound to a buyer creative: the buyer's `creative_id`, which now holds its assignments and delivery. Such a creative has no assignments of its own. |
| `revision_id` | string | Current buyer-authored input revision when the current content is revision-bound. Omitted for legacy/unversioned current content. |
| `seller_revision` | integer or string | Optional seller-maintained, monotonic, opaque change token, distinct from `revision_id`. Advances on every committed change to the creative from any origin. See [Seller revision](#seller-revision). Added in 3.3. |
| `name` | string | Human-readable name |
Expand Down Expand Up @@ -321,7 +322,7 @@ To manage one of these creatives, a buyer uses its `creative_id` as-is. See [sel

`platform_id` is the seller's identifier for the creative in its source of truth, the same value `sync_creatives` returns. It lets operators match a creative to the seller's own UI and reports without keeping the sync response. Buyers address creatives by `creative_id`, never by `platform_id`.

Delivery reporting uses the same seller-assigned `creative_id`. When such a creative delivers in a buyer's package, [`get_media_buy_delivery`](/docs/media-buy/task-reference/get_media_buy_delivery#creatives-the-buyer-didnt-sync) reports it in `by_package[].by_creative[]` under that ID, and [`get_creative_delivery`](/docs/creative/task-reference/get_creative_delivery) does too. To take one over as the buyer's own creative, see [Adopting a creative made outside AdCP](/docs/creative/task-reference/sync_creatives#adopting-a-creative-made-outside-adcp).
Delivery reporting uses the same seller-assigned `creative_id`. When such a creative delivers in a buyer's package, [`get_media_buy_delivery`](/docs/media-buy/task-reference/get_media_buy_delivery#creatives-the-buyer-didnt-sync) reports it in `by_package[].by_creative[]` under that ID, and [`get_creative_delivery`](/docs/creative/task-reference/get_creative_delivery) does too. To take one over as the buyer's own creative, see [Adopting a creative made outside AdCP](/docs/creative/task-reference/sync_creatives#adopting-a-creative-made-outside-adcp). After a library-level merge, the seller-assigned creative stays listed with `merged_into_creative_id` naming the buyer's creative, so the pairing can be read back at any time.

### Assignment indicators

Expand Down
64 changes: 60 additions & 4 deletions docs/creative/task-reference/sync_creatives.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1307,7 +1307,9 @@ If the seller needs a person to confirm a match, it returns the [`submitted` env

1. **Find it.** [`list_creatives`](/docs/creative/task-reference/list_creatives#creatives-created-outside-adcp) returns the creative under a seller-assigned `creative_id`, with the platform's own identifier in `platform_id`. With `include_assignments: true`, it also lists the packages the creative is assigned to. Confirm that the seller declares `creative.stable_creative_ids` and `creative.supports_assignment_merge`.
2. **Sync the buyer's copy.** Build a creative under the buyer's own `creative_id` from the same assets (media, text, click-through) in the same format, and sync it. A copy whose content differs is rejected at merge time with `content_mismatch`, and one in a different format with `format_mismatch`.
3. **Merge each assignment.** Send one `merge` per package the seller-assigned creative is assigned to. The seller keeps the running serving object, its approval and its delivery, and reports it under the buyer's `creative_id` from then on.
3. **Merge.** If the seller declares `creative.supports_library_merge`, send one `merge` without `package_id` (see [Merging across the account](#merging-across-the-account)). Otherwise send one `merge` per package the seller-assigned creative is assigned to. Either way, the seller keeps the running serving object, its approval and its delivery, and reports it under the buyer's `creative_id` from then on.

Per package:

```json
{
Expand All @@ -1331,10 +1333,64 @@ If the seller needs a person to confirm a match, it returns the [`submitted` env
}
```

Two limits apply in AdCP 3.3:
Two limits apply:

- **A package merge covers one package.** Each assignment needs its own `merge`. If the seller-assigned creative is assigned to another package later, for example because the advertiser adds the same creative to a new ad in the platform's tools, that assignment is reported under the seller-assigned ID until the buyer merges it too. Buyers that adopt creatives this way watch for new assignments through `creative.assignment_changed` notifications or by rereading `list_creatives`. A [library-level merge](#merging-across-the-account) removes this limit.
- **History stays split.** Delivery before each merge stays under the seller-assigned ID, with either kind of merge. To report the creative's whole life, the buyer joins the two IDs; see [delivery before and after a merge](/docs/media-buy/task-reference/get_media_buy_delivery#delivery-before-and-after-a-merge).

### Merging across the account

On many platforms one creative can run in many ads across many packages. A buyer adopting it wants to say once that the seller's creative is its own, so that every assignment, now and later, reports under the buyer's `creative_id`. A `merge` without `package_id` does that. It is a library-level merge: "`slr_7f3c2a91` is my `acme_spring_carousel`, everywhere in this account."

{/* Library-level merge is not yet released: these two examples use /schemas/latest/. Update to the major alias after release. */}

```json
{
"$schema": "https://adcontextprotocol.org/schemas/latest/creative/sync-creatives-request.json",
"account": { "account_id": "acct_acme_2026" },
"idempotency_key": "4a9e1c7b-2d5f-4f08-b6a3-8e0d2c5f1b97",
"assignment_operations": [{
"operation": "merge",
"creative_id": "acme_spring_carousel",
"merged_creative_id": "slr_7f3c2a91"
}]
}
```

Buyers MUST NOT send a `merge` without `package_id` unless the seller declares [`creative.supports_library_merge`](/docs/protocol/get_adcp_capabilities#creative). A seller that does not declare it rejects one with [`UNSUPPORTED_FEATURE`](/docs/building/verification/compliance-catalog#error-code-unsupported-feature), even if it declares `creative.supports_assignment_merge`. A seller that declares it:

- MUST verify the pairing as for a package merge, and MUST treat `merged_creative_id` as the same creative as `creative_id` everywhere in the account.
- MUST merge every current assignment of `merged_creative_id`, as a package merge would. Current means on a package that is not canceled, in a media buy that is not in a terminal state. Paused packages are included. Assignments in terminal media buys stay as they were, with their delivery history.
- MUST make every later assignment of that creative under `creative_id`. When the seller's source of truth assigns the same creative to another package, for example because the advertiser adds it to a new ad in the platform's tools, the seller reports that assignment in `list_creatives`, `get_media_buys` and delivery under `creative_id`, never under `merged_creative_id`. A seller that emits `creative.assignment_changed` sends `assigned` for `creative_id`.
- MUST keep listing `merged_creative_id` in `list_creatives` while it exists in the seller's source of truth, with no assignments and with `merged_into_creative_id` set to `creative_id`. The merge doesn't purge it, and it stays out of `delete_missing` as before.
- MUST report delivery under `creative_id` from the merge onward and MUST NOT restate earlier delivery, which stays under `merged_creative_id`. `merged_into_creative_id` is the durable record buyers use to [join the two IDs](/docs/media-buy/task-reference/get_media_buy_delivery#delivery-before-and-after-a-merge).

**Verification.** The library-level merge is all or nothing. If the seller can't merge any one current assignment, it applies no operation in the request and returns [`CREATIVE_MERGE_REJECTED`](/docs/building/verification/compliance-catalog#error-code-creative-merge-rejected) with one rejection per blocking package, each with its `package_id` and a `reason` (`format_mismatch` or `not_supported`). Reasons about the pairing itself carry no `package_id`:

| `reason` | Meaning | Who acts |
|---|---|---|
| `content_mismatch` | The seller judges that `merged_creative_id` is a different creative. | Nobody: the seller's creative is not this creative. |
| `not_in_account` | `merged_creative_id` is not a seller-assigned creative in this account. Unknown IDs get the same reason, so the response doesn't reveal whether an ID exists elsewhere. | The buyer: reread `list_creatives` and correct the pairing. |
| `already_merged` | An earlier library-level merge bound `merged_creative_id` to a different `creative_id`. A seller-assigned creative is bound to at most one buyer creative. | The buyer: read `merged_into_creative_id` in `list_creatives`. |

`not_on_package` doesn't apply to a library-level merge. Confirmation, `dry_run`, and atomicity with the rest of the request work as for a package merge.

**Response.** `merges[]` gets one entry without `package_id` for the account-wide binding, followed by one entry per assignment the merge moved, ordered by `package_id`, each with its inherited `approval_status`. A seller-assigned creative with no current assignments yields only the binding entry. Repeating a library-level merge of the same pair succeeds with only the binding entry.

```json
{
"$schema": "https://adcontextprotocol.org/schemas/latest/creative/sync-creatives-response.json",
"status": "completed",
"creatives": [],
"merges": [
{ "creative_id": "acme_spring_carousel", "merged_creative_id": "slr_7f3c2a91" },
{ "creative_id": "acme_spring_carousel", "package_id": "pkg_spring_retargeting", "merged_creative_id": "slr_7f3c2a91", "approval_status": "approved" },
{ "creative_id": "acme_spring_carousel", "package_id": "pkg_spring_social", "merged_creative_id": "slr_7f3c2a91", "approval_status": "approved" }
]
}
```

- **Merge is per package.** Each assignment needs its own `merge`. If the seller-assigned creative is assigned to another package later, for example because the advertiser adds the same creative to a new ad in the platform's tools, that assignment is reported under the seller-assigned ID until the buyer merges it too. Buyers that adopt creatives watch for new assignments through `creative.assignment_changed` notifications or by rereading `list_creatives`.
- **History stays split.** Delivery before each merge stays under the seller-assigned ID. To report the creative's whole life, the buyer joins the two IDs; see [delivery before and after a merge](/docs/media-buy/task-reference/get_media_buy_delivery#delivery-before-and-after-a-merge).
Each moved assignment produces the same `creative.assignment_changed` notifications and account change feed records as a package merge. A library-level merge has no undo. To stop running `creative_id` on a package, `unassign` it or `replace` it as usual.

### Dry run

Expand Down
2 changes: 1 addition & 1 deletion docs/media-buy/task-reference/get_media_buy_delivery.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,7 @@ To see the creative's whole life, the buyer joins the two IDs itself:
- Recompute rates such as `ctr` and `cpm` from those sums rather than averaging the rows' rates.
- Don't add `reach` or `frequency` across the two rows. The same person can be in both, and a buyer-side join can't remove the overlap. Read them per ID, or per day for days that fall wholly before or after the merge.

`list_creatives` and [`get_media_buys`](/docs/media-buy/task-reference/get_media_buys) don't show the pairing after the fact, so buyers keep it from the `merges[]` entries of the `sync_creatives` response (`merged_creative_id`, `creative_id`, `package_id`). A seller that emits `creative.assignment_changed` sends `unassigned` and `assigned` with the same `observed_at`, and a seller with the account change feed records two `creative_assignment` changes sharing one `batch_id`.
After a [library-level merge](/docs/creative/task-reference/sync_creatives#merging-across-the-account), `list_creatives` shows the pairing on the seller-assigned creative as `merged_into_creative_id`, and every later assignment reports under the buyer's ID. After a package merge, `list_creatives` and [`get_media_buys`](/docs/media-buy/task-reference/get_media_buys) don't show the pairing after the fact, so buyers keep it from the `merges[]` entries of the `sync_creatives` response (`merged_creative_id`, `creative_id`, `package_id`). A seller that emits `creative.assignment_changed` sends `unassigned` and `assigned` with the same `observed_at`, and a seller with the account change feed records two `creative_assignment` changes sharing one `batch_id`.

## Catalog-item reporting

Expand Down
Loading
Loading