Skip to content

feat(creative): merge a seller-assigned creative across the account - #8909

Draft
bokelley wants to merge 1 commit into
bokelley/creative-delivery-adoption-clarityfrom
bokelley/library-level-creative-merge
Draft

bokelley wants to merge 1 commit into
bokelley/creative-delivery-adoption-clarityfrom
bokelley/library-level-creative-merge

Conversation

@bokelley

@bokelley bokelley commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Draft. Not for 3.3. This is a proposal companion for #8897 (Gap 1 in the addendum), in the style of #7958. It is stacked on #8908: retarget it to main once #8908 merges.

Summary

A merge in sync_creatives assignment_operations works one package at a time. On platforms where one creative runs in many ads across many packages, typically self-serve social and search platforms, a buyer that adopts a creative made in the platform's own tools has two problems:

  • It needs one merge per package.
  • When the seller later assigns the same creative to another package, that assignment shows up under the seller's ID again.

A buyer platform building this on Meta hit both while building adoption.

This PR adds a library-level merge: a merge with no package_id. It declares the seller-assigned creative to be the buyer's creative everywhere in the account. It's gated by the new creative.supports_library_merge capability.

Wire changes

All changes are additive and capability-gated.

Schema Change
protocol/get-adcp-capabilities-response.json New creative.supports_library_merge (boolean, default false). Requires supports_assignment_merge: true, enforced with if/then.
creative/sync-creatives-request.json package_id is no longer required on merge. Leaving it out makes the merge library-level. x-adcp-validation gains a library capability gate, the two new reasons, and the library on-success rules.
creative/sync-creatives-response.json A merges[] entry may omit package_id. That entry records the account-wide binding. approval_status is required whenever package_id is present.
error-details/creative-merge-rejected.json package_id is optional. For a library-level merge, there is one entry per blocking package.
enums/creative-merge-rejection-reason.json New not_in_account and already_merged, both for library-level merges only.
creative/list-creatives-response.json New merged_into_creative_id on a seller-assigned creative that a library-level merge bound to a buyer creative.
enums/error-code.json The CREATIVE_MERGE_REJECTED text covers library-level merges. The snippet is regenerated.

Semantics

These are defined in sync_creatives.mdx, under Merging across the account. A seller that declares supports_library_merge:

  • verifies the pairing as for a package merge;
  • merges every current assignment of the seller-assigned creative. An assignment is current if its package isn't canceled and its media buy isn't terminal; paused packages count. The merge is atomic;
  • makes every later assignment of that creative under the buyer's creative_id, and never under the seller's ID;
  • keeps listing the seller-assigned creative, with no assignments and with merged_into_creative_id set;
  • reports delivery under the buyer's ID from the merge onward, and never restates earlier delivery.

The response carries one binding entry without package_id, then one entry per moved assignment, in package_id order. Repeating the same pair succeeds with only the binding entry. Package merges are unchanged.

History after a merge: open, one option recorded here

The #8897 addendum asked whether a merge should be able to ask the seller to restate the creative's earlier delivery under the buyer's ID (for example with restate_history: true), or whether buyers should join the two IDs themselves.

Not decided. The choice is between sellers never restating, where buyers join the two IDs, and an opt-in restatement that a seller declares with a capability. This draft records the first option: it adds merged_into_creative_id, which makes the join durable, and keeps the 3.3 rule that earlier delivery is not restated. An opt-in restatement could be layered on later. #8908 states the join rules for 3.3 package merges.

The case for the recorded option:

  • Restating changes delivery that is already final. A restated report differs depending on when it was pulled. It also collides with Reliable Reporting revisions (reporting_revision_id, immutable control totals) and with webhook windows the buyer has already received.
  • Restating changes what every reader of the account sees. That includes other buyer agents on the same account, not only the buyer that asked.
  • restate_history is a mutating-request field. An older seller that ignores it would deliver a different result than the buyer expects. Under rule 4 of the versioning rules (docs(versioning): state the rules for what changes between releases #8704), it would need its own capability on top of this one.
  • Joining is cheap for every metric except unique counts. Counts and money add across the two IDs, and rates are recomputed from the sums. Reach and frequency can't be joined across the merge boundary. Daily reach still works for every day that falls wholly before or after the merge, which covers the creative-fatigue use case.

If buyers later need deduplicated reach over a creative's whole life, a read-side option (report under the current ID) is the shape to consider. It changes no stored history.

Alternatives not picked

  • A new operation value (for example merge_library or adopt) instead of an optional package_id. The verification, errors and response are the same as for a package merge, so a scoping field is simpler. Dispatch also stays on operation alone.
  • Two merge branches told apart by whether package_id is present. This breaks the "dispatch is literal" principle in RFC: AdCP 4.0 — a fast, cleanup-first major (scope, principles, schedule) #8756.
  • Reusing not_on_package for library-level failures. Its meaning is package-specific, so a buyer couldn't tell which correction to make.

Versioning and process

  • Target. There is no 3.4 (RFC: AdCP 4.0 — a fast, cleanup-first major (scope, principles, schedule) #8756 retired the milestone). This is a candidate 4.0 rider, or a 4.x minor. x-added-in is set to 4.0.0 provisionally; set it to the release this lands in. Changeset: minor. Don't merge while main is in 3.3 pre mode.
  • Additive over 3.3. Removing package_id from required on the closed merge branch, and on the closed rejection entry, is not an additive-minor violation. The new properties are on open objects (creative capabilities, the list_creatives item, merges[] items). The enum only gains values. npm run test:additive-minor passes.
  • Rule 4. The new request shape is gated by a capability the seller declares first, and buyers MUST NOT send it otherwise. A 3.3 seller that validates requests rejects a merge without package_id, because package_id is required there.
  • RFC. This adds normative MUSTs, so per docs/governance/rfc-process.mdx it needs WG review and a decision record before merge. The proposal is get_media_buy_delivery: daily rows per creative, and creative rows for creatives the buyer did not assign #8897 and its addendum.

Open questions for the WG

  1. History after a merge. Should sellers never restate, as recorded here, or should an opt-in restatement exist (see above)?
  2. Undo. A library-level merge has no undo in this draft. Should it have one, or is unassign/replace per package enough?
  3. Terminal buys. Should assignments in media buys that are already terminal stay under the seller's ID (this draft), or be relabeled too?
  4. Stability. Stable-additive behind a capability (this draft), or experimental?
  5. platform_id. After a merge, the buyer's creative_id can be served by the seller's existing objects. Should list_creatives say anything about platform_id for it?

Test plan

  • npm run test:composed: seven new cases. They cover a library merge request, the binding plus moved-assignment readback, approval_status required with package_id, the new rejection reasons with and without a blocking package, the capability dependency, and merged_into_creative_id.
  • npm run test:schemas, npm run test:additive-minor, npm run test:oneof-discriminators, npm run test:error-codes, npm run test:error-code-drift
  • npm run build:compliance -- --check, npm run test:compliance-snippets
  • npm run test:mcp-schema-projection (prompt-view budget unchanged), npm run test:docs-nav, node tests/check-platform-agnostic.cjs
  • node tests/json-schema-validation.test.cjs --file on every changed page
  • check-changeset-protocol-scope and changeset status
  • Pre-commit hook, including the full server unit suite.

Refs #8897

🤖 Generated with Claude Code

A merge in sync_creatives assignment_operations works per package. On
platforms where one creative runs in many ads across many packages, a
buyer adopting it needs one merge per package, and an assignment the
seller makes later stays under the seller's ID.

A merge without package_id is now a library-level merge, gated by the
new creative.supports_library_merge capability (requires
supports_assignment_merge). The seller merges every current assignment,
makes every later assignment under the buyer's creative_id, and keeps
the seller-assigned creative listed with merged_into_creative_id, the
durable record buyers use to join delivery from before the merge.
Delivery before a merge is still not restated.

- sync-creatives-request: package_id optional on merge.
- sync-creatives-response: merges[] binding entry without package_id;
  approval_status required whenever package_id is present.
- creative-merge-rejected: package_id optional; names each blocking
  package for a library-level merge.
- creative-merge-rejection-reason: not_in_account, already_merged.
- list-creatives-response: merged_into_creative_id.
- Docs, normative spec bullet, error-code text, schema tests.

Refs #8897

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant