Skip to content

Commit 7f26ad1

Browse files
authored
feat: add creative format compatibility helpers (#887)
1 parent 50eb009 commit 7f26ad1

31 files changed

Lines changed: 604 additions & 98 deletions

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -916,14 +916,17 @@ async with ADCPClient(config) as client:
916916

917917
if media_buy_result.success:
918918
media_buy_id = media_buy_result.data.media_buy_id
919-
print(f"✅ Media buy created: {media_buy_id}")
919+
revision = media_buy_result.data.revision
920+
confirmed_at = media_buy_result.data.confirmed_at
921+
print(f"✅ Media buy created: {media_buy_id} at {confirmed_at}")
920922

921923
# 4. Update media buy if needed
922924
from adcp import UpdateMediaBuyPackagesRequest
923925

924926
update_result = await client.update_media_buy(
925927
UpdateMediaBuyPackagesRequest(
926928
media_buy_id=media_buy_id,
929+
revision=revision, # optimistic concurrency token from create/get/update
927930
packages=[{
928931
"package_id": product.packages[0].package_id,
929932
"quantity": 1500000 # Increase budget
@@ -932,9 +935,16 @@ async with ADCPClient(config) as client:
932935
)
933936

934937
if update_result.success:
938+
revision = update_result.data.revision
935939
print("✅ Media buy updated")
936940
```
937941

942+
`revision` is the media-buy concurrency token. Read it from `create_media_buy`,
943+
`get_media_buys`, or the last successful `update_media_buy`, then pass it on the
944+
next mutating update so the seller can reject stale writes. `confirmed_at` is the
945+
seller commitment timestamp and should remain stable across later pause/resume or
946+
budget updates.
947+
938948
### Complete Creative Workflow
939949

940950
Build and deliver production-ready creatives:

examples/sales_proposal_mode_seller/src/proposal_manager.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from typing import Any
2121

2222
from adcp.decisioning import (
23+
AdcpError,
2324
CapabilityOverlap,
2425
FinalizeProposalRequest,
2526
FinalizeProposalSuccess,
@@ -208,15 +209,27 @@ async def refine_products(
208209
{"ctv-premium-q2": 80.0, "display-run-q2": 20.0},
209210
)
210211
applied = []
211-
for entry in refines:
212+
for index, entry in enumerate(refines):
212213
inner = getattr(entry, "root", entry)
213214
scope = getattr(inner, "scope", None)
214215
scope_str = str(getattr(scope, "value", scope)) if scope is not None else None
215216
if scope_str == "proposal":
217+
proposal_id = str(getattr(inner, "proposal_id", PROPOSAL_ID))
218+
if proposal_id != PROPOSAL_ID:
219+
raise AdcpError(
220+
"PROPOSAL_NOT_FOUND",
221+
message=(
222+
f"Proposal {proposal_id!r} not found. Call get_products "
223+
"with buying_mode='brief' or a valid refine sequence "
224+
"to obtain a draft proposal_id before refining it."
225+
),
226+
recovery="correctable",
227+
field=f"refine[{index}].proposal_id",
228+
)
216229
applied.append(
217230
{
218231
"scope": "proposal",
219-
"proposal_id": str(getattr(inner, "proposal_id", PROPOSAL_ID)),
232+
"proposal_id": proposal_id,
220233
"status": "applied",
221234
"notes": "Adjusted CTV/display split per ask.",
222235
}

examples/seller_agent.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ def _image_format_options(
228228
{
229229
"pricing_option_id": "po-cpm-homepage",
230230
"pricing_model": "cpm",
231-
"floor_price": 15.00,
231+
"fixed_price": 15.00,
232232
"currency": "USD",
233233
}
234234
],
@@ -260,7 +260,7 @@ def _image_format_options(
260260
{
261261
"pricing_option_id": "po-cpm-ros",
262262
"pricing_model": "cpm",
263-
"floor_price": 5.00,
263+
"fixed_price": 5.00,
264264
"currency": "USD",
265265
}
266266
],
@@ -295,7 +295,7 @@ def _image_format_options(
295295
{
296296
"pricing_option_id": "cpm_standard",
297297
"pricing_model": "cpm",
298-
"floor_price": 5.00,
298+
"fixed_price": 5.00,
299299
"currency": "USD",
300300
}
301301
],
@@ -327,7 +327,7 @@ def _image_format_options(
327327
{
328328
"pricing_option_id": "cpm_standard",
329329
"pricing_model": "cpm",
330-
"floor_price": 8.00,
330+
"fixed_price": 8.00,
331331
"currency": "USD",
332332
}
333333
],
@@ -359,7 +359,7 @@ def _image_format_options(
359359
{
360360
"pricing_option_id": "cpm_guaranteed",
361361
"pricing_model": "cpm",
362-
"floor_price": 25.00,
362+
"fixed_price": 25.00,
363363
"currency": "USD",
364364
}
365365
],
@@ -391,7 +391,7 @@ def _image_format_options(
391391
{
392392
"pricing_option_id": "cpm_standard",
393393
"pricing_model": "cpm",
394-
"floor_price": 6.00,
394+
"fixed_price": 6.00,
395395
"currency": "USD",
396396
}
397397
],
@@ -657,6 +657,12 @@ async def update_media_buy(self, params: dict[str, Any], context: Any = None) ->
657657
mb_id = params.get("media_buy_id")
658658
mb = media_buys.get(mb_id) if mb_id else None
659659
if not mb or not mb_id:
660+
if any(pkg.get("package_id") for pkg in params.get("packages") or []):
661+
return adcp_error(
662+
"PACKAGE_NOT_FOUND",
663+
f"Package not found in media buy {mb_id}",
664+
field="package_id",
665+
)
660666
return adcp_error("MEDIA_BUY_NOT_FOUND", f"Media buy {mb_id} not found")
661667

662668
if params.get("revision") and params["revision"] != mb.get("revision", 1):
@@ -826,6 +832,13 @@ async def get_media_buy_delivery(
826832
"impressions": 45000,
827833
"clicks": 680,
828834
"spend": 540.00,
835+
"viewability": {
836+
"measurable_impressions": 42000,
837+
"viewable_impressions": 31500,
838+
"viewable_rate": 0.75,
839+
"viewed_seconds": 12.5,
840+
"standard": "mrc",
841+
},
829842
},
830843
"by_package": [],
831844
}
@@ -875,7 +888,13 @@ async def force_creative_status(
875888
) -> dict[str, Any]:
876889
c = creatives.get(creative_id)
877890
if not c:
878-
raise TestControllerError("NOT_FOUND", f"Creative {creative_id} not found")
891+
c = {
892+
"creative_id": creative_id,
893+
"name": creative_id,
894+
"format_id": {"agent_url": AGENT_URL, "id": "display_300x250"},
895+
"status": "unknown",
896+
}
897+
creatives[creative_id] = c
879898
prev = c.get("status", "unknown")
880899
if prev == "archived":
881900
raise TestControllerError(

schemas/cache/3.1.0-beta.4/bundled/core/tasks-get-response.json

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46502,9 +46502,12 @@
4650246502
"$ref": "#/$defs/MediaBuyStatus"
4650346503
},
4650446504
"confirmed_at": {
46505-
"type": "string",
46505+
"type": [
46506+
"string",
46507+
"null"
46508+
],
4650646509
"format": "date-time",
46507-
"description": "ISO 8601 timestamp when this media buy was confirmed by the seller. A successful create_media_buy response constitutes order confirmation."
46510+
"description": "Seller commitment timestamp for this media buy. This is the time the seller confirmed the order, not a delivery-state timestamp; once set it remains stable across pause, resume, budget, and package updates. Pending/manual approval flows may leave it null until seller commitment happens."
4650846511
},
4650946512
"creative_deadline": {
4651046513
"type": "string",
@@ -46513,7 +46516,7 @@
4651346516
},
4651446517
"revision": {
4651546518
"type": "integer",
46516-
"description": "Initial revision number for this media buy. Use in subsequent update_media_buy requests for optimistic concurrency.",
46519+
"description": "Initial optimistic-concurrency revision for this media buy, usually 1 when the seller mints the buy synchronously. Clients should pass the last observed revision on update_media_buy.",
4651746520
"minimum": 1
4651846521
},
4651946522
"currency": {
@@ -54506,7 +54509,7 @@
5450654509
},
5450754510
"revision": {
5450854511
"type": "integer",
54509-
"description": "Revision number after this update. Use this value in subsequent update_media_buy requests for optimistic concurrency.",
54512+
"description": "Optimistic-concurrency revision after this mutating update. Use this new value in the next update_media_buy request; reload the media buy and retry if a seller rejects an update because the supplied revision is stale.",
5451054513
"minimum": 1
5451154514
},
5451254515
"currency": {
@@ -104973,4 +104976,4 @@
104973104976
"generatedAt": "2026-05-26T03:04:14.411Z",
104974104977
"note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory."
104975104978
}
104976-
}
104979+
}

schemas/cache/3.1.0-beta.4/bundled/media-buy/create-media-buy-response.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1914,9 +1914,12 @@
19141914
"$ref": "#/$defs/MediaBuyStatus"
19151915
},
19161916
"confirmed_at": {
1917-
"type": "string",
1917+
"type": [
1918+
"string",
1919+
"null"
1920+
],
19181921
"format": "date-time",
1919-
"description": "ISO 8601 timestamp when this media buy was confirmed by the seller. A successful create_media_buy response constitutes order confirmation."
1922+
"description": "Seller commitment timestamp for this media buy. This is the time the seller confirmed the order, not a delivery-state timestamp; once set it remains stable across pause, resume, budget, and package updates. Pending/manual approval flows may leave it null until seller commitment happens."
19201923
},
19211924
"creative_deadline": {
19221925
"type": "string",
@@ -1925,7 +1928,7 @@
19251928
},
19261929
"revision": {
19271930
"type": "integer",
1928-
"description": "Initial revision number for this media buy. Use in subsequent update_media_buy requests for optimistic concurrency.",
1931+
"description": "Initial optimistic-concurrency revision for this media buy, usually 1 when the seller mints the buy synchronously. Clients should pass the last observed revision on update_media_buy.",
19291932
"minimum": 1
19301933
},
19311934
"currency": {
@@ -9976,4 +9979,4 @@
99769979
"generatedAt": "2026-05-26T03:04:14.740Z",
99779980
"note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory."
99789981
}
9979-
}
9982+
}

schemas/cache/3.1.0-beta.4/bundled/media-buy/update-media-buy-response.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@
440440
},
441441
"revision": {
442442
"type": "integer",
443-
"description": "Revision number after this update. Use this value in subsequent update_media_buy requests for optimistic concurrency.",
443+
"description": "Optimistic-concurrency revision after this mutating update. Use this new value in the next update_media_buy request; reload the media buy and retry if a seller rejects an update because the supplied revision is stale.",
444444
"minimum": 1
445445
},
446446
"currency": {
@@ -7609,4 +7609,4 @@
76097609
"generatedAt": "2026-05-26T03:04:14.907Z",
76107610
"note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory."
76117611
}
7612-
}
7612+
}

schemas/cache/3.1.0-beta.4/media-buy/create-media-buy-response.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,12 @@
4040
"description": "DEPRECATED in 3.1, removed in 3.2 (#4906). Use `media_buy_status` instead. Top-level `status` here collides with the envelope TaskStatus on flat-serialized MCP wire (see adcontextprotocol/adcp#4895). Buyers consuming 3.1+ responses MUST prefer `media_buy_status` when present; sellers MAY emit both during the deprecation window but MUST emit identical values when doing so \u2014 divergent emission is a conformance violation."
4141
},
4242
"confirmed_at": {
43-
"type": "string",
43+
"type": [
44+
"string",
45+
"null"
46+
],
4447
"format": "date-time",
45-
"description": "ISO 8601 timestamp when this media buy was confirmed by the seller. A successful create_media_buy response constitutes order confirmation."
48+
"description": "Seller commitment timestamp for this media buy. This is the time the seller confirmed the order, not a delivery-state timestamp; once set it remains stable across pause, resume, budget, and package updates. Pending/manual approval flows may leave it null until seller commitment happens."
4649
},
4750
"creative_deadline": {
4851
"type": "string",
@@ -51,7 +54,7 @@
5154
},
5255
"revision": {
5356
"type": "integer",
54-
"description": "Initial revision number for this media buy. Use in subsequent update_media_buy requests for optimistic concurrency.",
57+
"description": "Initial optimistic-concurrency revision for this media buy, usually 1 when the seller mints the buy synchronously. Clients should pass the last observed revision on update_media_buy.",
5558
"minimum": 1
5659
},
5760
"currency": {
@@ -222,4 +225,4 @@
222225
}
223226
],
224227
"properties": {}
225-
}
228+
}

schemas/cache/3.1.0-beta.4/media-buy/get-media-buys-response.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,12 @@
8080
"description": "ISO 8601 timestamp for creative upload deadline"
8181
},
8282
"confirmed_at": {
83-
"type": "string",
83+
"type": [
84+
"string",
85+
"null"
86+
],
8487
"format": "date-time",
85-
"description": "ISO 8601 timestamp when the seller confirmed this media buy. A successful create_media_buy response constitutes order confirmation."
88+
"description": "Seller commitment timestamp for this media buy. This is the time the seller confirmed the order, not a delivery-state timestamp; once set it remains stable across pause, resume, budget, and package updates. Pending/manual approval flows may leave it null until seller commitment happens."
8689
},
8790
"cancellation": {
8891
"type": "object",
@@ -111,7 +114,7 @@
111114
},
112115
"revision": {
113116
"type": "integer",
114-
"description": "Current revision number. Pass this in update_media_buy for optimistic concurrency.",
117+
"description": "Current optimistic-concurrency revision for this media buy. Pass this value in update_media_buy; reload and retry if the seller reports a stale revision conflict.",
115118
"minimum": 1
116119
},
117120
"created_at": {
@@ -434,4 +437,4 @@
434437
"media_buys"
435438
],
436439
"additionalProperties": true
437-
}
440+
}

schemas/cache/3.1.0-beta.4/media-buy/update-media-buy-response.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
},
3434
"revision": {
3535
"type": "integer",
36-
"description": "Revision number after this update. Use this value in subsequent update_media_buy requests for optimistic concurrency.",
36+
"description": "Optimistic-concurrency revision after this mutating update. Use this new value in the next update_media_buy request; reload the media buy and retry if a seller rejects an update because the supplied revision is stale.",
3737
"minimum": 1
3838
},
3939
"currency": {
@@ -202,4 +202,4 @@
202202
}
203203
],
204204
"properties": {}
205-
}
205+
}

src/adcp/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@
4242
verify_agent_authorization,
4343
verify_agent_for_property,
4444
)
45+
from adcp.canonical_formats import (
46+
format_is_supported,
47+
formats_are_equivalent,
48+
upgrade_legacy_format_id,
49+
)
4550
from adcp.capabilities import ( # noqa: F401
4651
FeatureResolver,
4752
build_synthetic_capabilities,
@@ -819,6 +824,9 @@ def get_adcp_version() -> str:
819824
"Error",
820825
"Format",
821826
"FormatId",
827+
"format_is_supported",
828+
"formats_are_equivalent",
829+
"upgrade_legacy_format_id",
822830
"FormatOptionReference",
823831
"AssetContentType",
824832
"Product",

0 commit comments

Comments
 (0)