Skip to content
Merged
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
13 changes: 13 additions & 0 deletions .changeset/installment-creative-assignment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"adcontextprotocol": minor
---

Add experimental `installment_refs` to the `assign` and `replace` operations of `sync_creatives` `assignment_operations` (`media_buy.installment_creative_assignment`, #8968). A buyer can now give each installment of a package its own creative, such as the awareness brief on a creator's first post and the offer on the second. Each entry is a `core/canonical-installment-ref.json`, with unique items and at most 500, matching a product's installments. Absent means every installment, as before.

- **Rotation.** Within one installment, weight, rotation and placement routing work unchanged over the assignments that apply to it. Placement routing and installment scope intersect. An installment no assignment applies to gets no creative scoped to another installment.
- **Seller rules.** Each ref MUST name an installment in the `installments` of the purchased product. Any other ref is rejected with `VALIDATION_ERROR`. An operation that changes an installment past its `deadlines.change_cutoff` is rejected with `CREATIVE_DEADLINE_EXCEEDED`, naming the installment, and is never accepted silently. No new error codes.
- **Scope changes.** A supplied list replaces the assignment's scope, and omission on an `assign` upsert keeps it. `replace` transfers the replaced assignment's scope unless it supplies one, and `merge` moves it. `unassign` and `merge` reject `installment_refs` by name.
- **Readback.** The same field appears on `get_media_buys` `packages[].creative_approvals[]` and `list_creatives` `assignments.assigned_packages[]`, and is omitted when the assignment applies to every installment.
- **Capability.** Buyers send the field only to sellers that list `media_buy.installment_creative_assignment` in `experimental_features`, because a seller that ignored it would run the creative on every installment (versioning rule 4). A seller that does not support it rejects it with `UNSUPPORTED_FEATURE`.

The field is not on `core/creative-assignment.json`, the deprecated `create_media_buy` and `update_media_buy` package rows, or the deprecated `sync_creatives` `assignments[]` row. 3.3 adds nothing to deprecated tasks (#8501). Date windows (`valid_from` / `valid_until`) for packages without installments are a follow-up.
2 changes: 2 additions & 0 deletions docs/creative/task-reference/list_creatives.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,8 @@ In AdCP 3.3, sellers advertising `creative.supports_assignment_trackers` MUST re

In AdCP 3.3, `assignments.assigned_packages[].landing_page_url` reports the effective serving destination for that creative on that package. Sellers advertising `creative.supports_assignment_landing_page` return the assignment override, or the creative's own destination when unique. Macro-bearing URLs retain the effective template. Omit the scalar when no destination exists or an unoverridden creative has multiple destinations; read those from the creative's `assets` rather than treating an arbitrary destination as applying to every click. The creative's own `status` and assignment `approval_status` remain separate. See [assignment landing pages](/docs/creative/task-reference/sync_creatives#assignment-landing-pages).

In AdCP 3.3, sellers that list the experimental `media_buy.installment_creative_assignment` return `assignments.assigned_packages[].installment_refs` on every assignment limited to some installments of the package, and omit it when the assignment applies to every installment. See [assigning to specific installments](/docs/creative/task-reference/sync_creatives#assigning-to-specific-installments-experimental).

Creative-library sellers that advertise `list_creatives` in `media_buy.relationship_notifications.projection_tasks` attach evaluated indicator state to `assignments.assigned_packages[]` and include `media_buy_id` plus `approval_status` on every row, including unknown rows. These fields mirror [`get_media_buys`](/docs/media-buy/task-reference/get_media_buys) for the same relationship. A mixed publisher/placement outcome uses `approval_status: "partially_approved"` plus complete `approval_scopes[]`; the assignment webhook remains a compact invalidation and buyers reread these scopes. Each evaluated row includes `indicator_types_evaluated`, `indicators_as_of`, and `indicators[]`; omitted types remain unknown.

An assignment row MAY carry `approval_status_as_of`: when the seller last refreshed `approval_status`, `rejection_reason`, and `approval_scopes` for that relationship from its source of truth. It follows the same rules as `creative_approvals[].approval_status_as_of` on `get_media_buys`. When both reads carry it for the same relationship, the values agree. See [creative state freshness](/docs/media-buy/specification#creative-state-freshness).
Expand Down
54 changes: 51 additions & 3 deletions docs/creative/task-reference/sync_creatives.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ A product can declare a separate `creative_policy.review_sla` for time to an app
| `creatives` | Creative[] | Conditional | Creative assets to upload/update (max 100); at least one of `creatives`, `assignments`, or `assignment_operations` is required. |
| `creative_ids` | string[] | No | Optional filter to limit sync scope to specific creative IDs. Only these creatives are affected, others remain untouched. Useful for partial updates and error recovery. |
| `assignments` | array | No | Deprecated additive `{creative_id, package_id}` shorthand. Optional `weight` and `placement_ids` per assignment. It does not carry `landing_page_url` or `trackers`; send those on an `assign` operation. |
| `assignment_operations` | array | Conditional | Ordered `assign`, `unassign`, or `replace` operations for existing creative IDs, without requiring creative uploads. Only `assign` accepts capability-gated `landing_page_url` and `trackers`; `replace` transfers the replaced assignment's settings, including those, to the replacement ([Replacing a creative on a package](#replacing-a-creative-on-a-package)). The request is atomic and requires strict validation. |
| `assignment_operations` | array | Conditional | Ordered `assign`, `unassign`, or `replace` operations for existing creative IDs, without requiring creative uploads. Only `assign` accepts capability-gated `landing_page_url` and `trackers`; `replace` transfers the replaced assignment's settings, including those, to the replacement ([Replacing a creative on a package](#replacing-a-creative-on-a-package)). `assign` and `replace` also accept experimental `installment_refs` ([Assigning to specific installments](#assigning-to-specific-installments-experimental)). The request is atomic and requires strict validation. |
| `dry_run` | boolean | No | When true, rehearse this exact sync operation without applying it (default: false). Use this for seller trafficking acceptance checks; use [`validate_input`](https://adcontextprotocol.org/schemas/v3/creative/validate-input-request.json) for manifest-only preflight against canonical/product format targets. |
| `validation_mode` | string | No | Validation strictness: `"strict"` (default) or `"lenient"` |
| `delete_missing` | boolean | No | When true, creatives not in this sync are archived (default: false). Cannot be combined with `creative_ids`. Cannot delete creatives assigned to active, non-paused packages. Sellers that declare `creative.stable_creative_ids` never archive [seller-created creatives](#seller-assigned-creative-ids); others may. |
Expand Down Expand Up @@ -259,9 +259,10 @@ The replacement takes over the replaced assignment's authored settings, so a swa

- `weight` and placement routing (`placement_ids` and `placement_refs`);
- rotation settings: `rotation_mode`, `group_id`, and `sequence_position`;
- assignment [`trackers`](#assignment-trackers) and the [`landing_page_url`](#assignment-landing-pages) override.
- assignment [`trackers`](#assignment-trackers) and the [`landing_page_url`](#assignment-landing-pages) override;
- the experimental installment scope, [`installment_refs`](#assigning-to-specific-installments-experimental).

A `weight` supplied on the operation replaces the transferred weight. A `placement_ids` supplied on the operation replaces the transferred placement routing entirely: neither the replaced assignment's `placement_ids` nor its `placement_refs` transfer. To change the transferred trackers or landing page in the same request, follow the `replace` with an `assign` for the replacement creative; operations apply in order and the request is atomic.
A `weight` supplied on the operation replaces the transferred weight. So does a supplied `installment_refs` for the transferred installment scope. A `placement_ids` supplied on the operation replaces the transferred placement routing entirely: neither the replaced assignment's `placement_ids` nor its `placement_refs` transfer. To change the transferred trackers or landing page in the same request, follow the `replace` with an `assign` for the replacement creative; operations apply in order and the request is atomic.

The seller MUST validate every transferred setting against the replacement, including tracker-slot capacity and events for its format, whether the landing-page override can apply to its clicks, and any destination review, and MUST reject the atomic request with the existing assignment errors rather than drop or partially apply a setting. Readback in [`list_creatives`](/docs/creative/task-reference/list_creatives) and [`get_media_buys`](/docs/media-buy/task-reference/get_media_buys) `creative_approvals[]` shows the transferred settings on the replacement's row.

Expand All @@ -281,6 +282,53 @@ The seller MUST validate every transferred setting against the replacement, incl
}
```

### Assigning to specific installments (experimental)

In AdCP 3.3, optional `installment_refs` on an `assign` or `replace` operation limits the assignment to some [installments](/docs/media-buy/product-discovery/collections-and-installments#the-installment-object) of the package: one post of a creator package, one episode's host read, one issue of a print run. Without it the assignment applies to every installment, as before. Each entry is a [canonical installment reference](https://adcontextprotocol.org/schemas/latest/core/canonical-installment-ref.json), a `collection_ref` and an `installment_id`.

Buyers MUST NOT send `installment_refs` unless the seller lists `media_buy.installment_creative_assignment` in `experimental_features` on [`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities). A seller that ignored the field would run the creative on every installment, so it waits on the capability ([versioning rule 4](/docs/reference/versioning#what-can-change-in-a-release)). A seller that does not support it MUST reject it with [`UNSUPPORTED_FEATURE`](/docs/building/verification/compliance-catalog#error-code-unsupported-feature). Send it only on `assignment_operations`: the deprecated `assignments[]` row and the `creative_assignments` rows of the deprecated `create_media_buy` and `update_media_buy` don't carry it.

- **Which installments.** Each ref MUST name an installment the package bought: one in the `installments` of the product its purchase references. The seller rejects any other ref with [`VALIDATION_ERROR`](/docs/building/verification/compliance-catalog#error-code-validation-error), with `error.field` at that ref, such as `assignment_operations[1].installment_refs[0]`. A run-of-collection product names no installments, so it has none to scope to.
- **Rotation and routing.** Within one installment, weight, rotation and placement routing work as they do today, over the assignments that apply to that installment. Placement routing and installment scope intersect: a creative with both serves only in those placements of those installments. An installment that no assignment applies to has no creative from this package, and the seller MUST NOT serve it a creative scoped to another installment.
- **Change cutoff.** An operation changes an installment when it adds or removes a creative there, or changes the weight or routing of one that applies there. An assignment without `installment_refs` applies to every installment, so changing it changes them all, and a `replace` changes every installment either creative applies to. When an operation changes an installment whose [`deadlines.change_cutoff`](/docs/media-buy/product-discovery/collections-and-installments#installment-deadlines) has passed, the seller MUST reject the request with [`CREATIVE_DEADLINE_EXCEEDED`](/docs/building/verification/compliance-catalog#error-code-creative-deadline-exceeded) and name the installment, with `error.field` at the ref when the operation names it, otherwise at the operation. It MUST NOT accept the change silently. The request is atomic, so it doesn't apply the change to the other installments alone either. The package's `creative_deadline` applies as before.
- **Changing the scope.** A supplied `installment_refs` replaces the assignment's installment scope, and omitting it on an `assign` upsert keeps the scope. To return an assignment to every installment, use the atomic `unassign`/`assign` [reset recipe](#assignment-landing-pages) without the field. A `replace` carries the replaced assignment's scope over unless it supplies its own. `unassign` and `merge` reject `installment_refs`: an `unassign` removes the creative from every installment it applied to, so to drop one installment, `assign` the remaining ones.
- **Readback.** Sellers that list the feature return `installment_refs` on every assignment that has a scope, in [`get_media_buys`](/docs/media-buy/task-reference/get_media_buys) `packages[].creative_approvals[]` and [`list_creatives`](/docs/creative/task-reference/list_creatives) `assignments.assigned_packages[]`. They omit it when the assignment applies to every installment.

This request gives each post of a two-post creator package its own creative: the awareness brief for the first post and the offer, with its discount code, for the second. It uses the unreleased experimental `installment_refs`; the installment creative assignment tests validate it against the source schema.

```json
{
"account": { "account_id": "acct_acme" },
"idempotency_key": "juniper-vale-posts-0001",
"assignment_operations": [
{
"operation": "assign",
"creative_id": "juniper_awareness_brief",
"package_id": "pkg_juniper_vale",
"installment_refs": [
{
"collection_ref": { "publisher_domain": "creatorloop.example", "collection_id": "creatorloop_channel_juniper_vale" },
"installment_id": "juniper_vale_slot_1"
}
]
},
{
"operation": "assign",
"creative_id": "juniper_offer_brief",
"package_id": "pkg_juniper_vale",
"installment_refs": [
{
"collection_ref": { "publisher_domain": "creatorloop.example", "collection_id": "creatorloop_channel_juniper_vale" },
"installment_id": "juniper_vale_slot_2"
}
]
}
]
}
```

To change the creative for later installments once an earlier one is past its cutoff, narrow the current assignment to the installments past it and assign the new creative to the rest. A `replace` of an assignment without `installment_refs` would change the earlier installments too, and the seller would refuse it.

### Creative object

| Field | Type | Required | Description |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,14 @@ For each product named, the **selected set** is that product's installments with

A committed proposal can read back which installments its hold covers, in `holds[]` (experimental, `media_buy.installment_holds`); see [Installment holds](/docs/media-buy/task-reference/refine_proposals#installment-holds-experimental). `holds[]` is readback about the hold finalize placed, not contract, and is not covered by `terms_digest`.

### Creatives per installment (experimental)

A package that buys several installments can give each one its own creative. A creator package with two posts is the usual case: the first post carries the awareness brief, and the second, weeks later, carries the call to action and a discount code. The same goes for a host read that changes by episode, or a print ad that changes by issue.

For a creator deal, the creative the buyer assigns is often the brief: talking points, claims to avoid and the required disclosure, which the creator turns into a draft. Sync each brief as a library creative, then assign it to its post with `installment_refs` on a [`sync_creatives`](/docs/creative/task-reference/sync_creatives) `assign` operation. That post's `change_cutoff` then applies to changing its brief, and its `material_deadlines` say when the seller needs the talking points, script or draft.

Each ref names one of the installments the package bought, the `installments` of the purchased product: the selected set, when the buyer [chose installments](#choosing-installments-experimental). An assignment without `installment_refs` applies to every installment, as before. A change that reaches an installment past its `change_cutoff` is refused, never accepted silently. Sellers that support this list `media_buy.installment_creative_assignment` in `experimental_features`, and buyers send the field only to them. See [Assigning to specific installments](/docs/creative/task-reference/sync_creatives#assigning-to-specific-installments-experimental) for the full rules.

## get_products response structure

Products reference collections via `collections` selectors — the same pattern as `publisher_properties`. Each selector has a `publisher_domain` and `collection_ids` array pointing to collections declared in that publisher's `adagents.json`. When the request sets `include_collections: true`, the response can also carry the full collection objects, described [below](#collections-in-responses).
Expand Down
1 change: 1 addition & 0 deletions docs/media-buy/task-reference/get_media_buys.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ been deleted. `ext.streamhaus.ad_set_id` here is a seller-defined extension:
| `creative_id` | Creative identifier |
| `trackers` | Added in 3.3. Complete assignment-scoped measurement tracker list, preserving events, URL templates, order, and duplicate occurrences. Supporting sellers MUST return it, using `[]` when none exist. Excludes the creative's own trackers in `assets`; both sets fire. See [assignment trackers](/docs/creative/task-reference/sync_creatives#assignment-trackers). |
| `landing_page_url` | Added in 3.3. Effective serving destination for this creative on this package, including an assignment override. Supporting sellers return the URL (or macro-bearing template) when unique; omit when no destination exists or an unoverridden creative has multiple destinations, whose URLs remain in its creative assets. See [assignment landing pages](/docs/creative/task-reference/sync_creatives#assignment-landing-pages). |
| `installment_refs` | Added in 3.3, experimental (`media_buy.installment_creative_assignment`). The installments this assignment is limited to. Sellers that list the feature return it on every assignment with an installment scope and omit it when the assignment applies to every installment. See [assigning to specific installments](/docs/creative/task-reference/sync_creatives#assigning-to-specific-installments-experimental). |
| `approval_status` | `pending_review`, `approved`, `partially_approved`, or `rejected` |
| `rejection_reason` | Explanation of rejection (when `approval_status` is `rejected`) |
| `approval_scopes` | Complete publisher/placement outcomes when `approval_status` is `partially_approved` |
Expand Down
Loading
Loading