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
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -916,14 +916,17 @@ async with ADCPClient(config) as client:

if media_buy_result.success:
media_buy_id = media_buy_result.data.media_buy_id
print(f"✅ Media buy created: {media_buy_id}")
revision = media_buy_result.data.revision
confirmed_at = media_buy_result.data.confirmed_at
print(f"✅ Media buy created: {media_buy_id} at {confirmed_at}")

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

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

if update_result.success:
revision = update_result.data.revision
print("✅ Media buy updated")
```

`revision` is the media-buy concurrency token. Read it from `create_media_buy`,
`get_media_buys`, or the last successful `update_media_buy`, then pass it on the
next mutating update so the seller can reject stale writes. `confirmed_at` is the
seller commitment timestamp and should remain stable across later pause/resume or
budget updates.

### Complete Creative Workflow

Build and deliver production-ready creatives:
Expand Down
17 changes: 15 additions & 2 deletions examples/sales_proposal_mode_seller/src/proposal_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from typing import Any

from adcp.decisioning import (
AdcpError,
CapabilityOverlap,
FinalizeProposalRequest,
FinalizeProposalSuccess,
Expand Down Expand Up @@ -208,15 +209,27 @@ async def refine_products(
{"ctv-premium-q2": 80.0, "display-run-q2": 20.0},
)
applied = []
for entry in refines:
for index, entry in enumerate(refines):
inner = getattr(entry, "root", entry)
scope = getattr(inner, "scope", None)
scope_str = str(getattr(scope, "value", scope)) if scope is not None else None
if scope_str == "proposal":
proposal_id = str(getattr(inner, "proposal_id", PROPOSAL_ID))
if proposal_id != PROPOSAL_ID:
raise AdcpError(
"PROPOSAL_NOT_FOUND",
message=(
f"Proposal {proposal_id!r} not found. Call get_products "
"with buying_mode='brief' or a valid refine sequence "
"to obtain a draft proposal_id before refining it."
),
recovery="correctable",
field=f"refine[{index}].proposal_id",
)
applied.append(
{
"scope": "proposal",
"proposal_id": str(getattr(inner, "proposal_id", PROPOSAL_ID)),
"proposal_id": proposal_id,
"status": "applied",
"notes": "Adjusted CTV/display split per ask.",
}
Expand Down
33 changes: 26 additions & 7 deletions examples/seller_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ def _image_format_options(
{
"pricing_option_id": "po-cpm-homepage",
"pricing_model": "cpm",
"floor_price": 15.00,
"fixed_price": 15.00,
"currency": "USD",
}
],
Expand Down Expand Up @@ -260,7 +260,7 @@ def _image_format_options(
{
"pricing_option_id": "po-cpm-ros",
"pricing_model": "cpm",
"floor_price": 5.00,
"fixed_price": 5.00,
"currency": "USD",
}
],
Expand Down Expand Up @@ -295,7 +295,7 @@ def _image_format_options(
{
"pricing_option_id": "cpm_standard",
"pricing_model": "cpm",
"floor_price": 5.00,
"fixed_price": 5.00,
"currency": "USD",
}
],
Expand Down Expand Up @@ -327,7 +327,7 @@ def _image_format_options(
{
"pricing_option_id": "cpm_standard",
"pricing_model": "cpm",
"floor_price": 8.00,
"fixed_price": 8.00,
"currency": "USD",
}
],
Expand Down Expand Up @@ -359,7 +359,7 @@ def _image_format_options(
{
"pricing_option_id": "cpm_guaranteed",
"pricing_model": "cpm",
"floor_price": 25.00,
"fixed_price": 25.00,
"currency": "USD",
}
],
Expand Down Expand Up @@ -391,7 +391,7 @@ def _image_format_options(
{
"pricing_option_id": "cpm_standard",
"pricing_model": "cpm",
"floor_price": 6.00,
"fixed_price": 6.00,
"currency": "USD",
}
],
Expand Down Expand Up @@ -657,6 +657,12 @@ async def update_media_buy(self, params: dict[str, Any], context: Any = None) ->
mb_id = params.get("media_buy_id")
mb = media_buys.get(mb_id) if mb_id else None
if not mb or not mb_id:
if any(pkg.get("package_id") for pkg in params.get("packages") or []):
return adcp_error(
"PACKAGE_NOT_FOUND",
f"Package not found in media buy {mb_id}",
field="package_id",
)
return adcp_error("MEDIA_BUY_NOT_FOUND", f"Media buy {mb_id} not found")

if params.get("revision") and params["revision"] != mb.get("revision", 1):
Expand Down Expand Up @@ -826,6 +832,13 @@ async def get_media_buy_delivery(
"impressions": 45000,
"clicks": 680,
"spend": 540.00,
"viewability": {
"measurable_impressions": 42000,
"viewable_impressions": 31500,
"viewable_rate": 0.75,
"viewed_seconds": 12.5,
"standard": "mrc",
},
},
"by_package": [],
}
Expand Down Expand Up @@ -875,7 +888,13 @@ async def force_creative_status(
) -> dict[str, Any]:
c = creatives.get(creative_id)
if not c:
raise TestControllerError("NOT_FOUND", f"Creative {creative_id} not found")
c = {
"creative_id": creative_id,
"name": creative_id,
"format_id": {"agent_url": AGENT_URL, "id": "display_300x250"},
"status": "unknown",
}
creatives[creative_id] = c
prev = c.get("status", "unknown")
if prev == "archived":
raise TestControllerError(
Expand Down
13 changes: 8 additions & 5 deletions schemas/cache/3.1.0-beta.4/bundled/core/tasks-get-response.json
Original file line number Diff line number Diff line change
Expand Up @@ -46502,9 +46502,12 @@
"$ref": "#/$defs/MediaBuyStatus"
},
"confirmed_at": {
"type": "string",
"type": [
"string",
"null"
],
"format": "date-time",
"description": "ISO 8601 timestamp when this media buy was confirmed by the seller. A successful create_media_buy response constitutes order confirmation."
"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."
},
"creative_deadline": {
"type": "string",
Expand All @@ -46513,7 +46516,7 @@
},
"revision": {
"type": "integer",
"description": "Initial revision number for this media buy. Use in subsequent update_media_buy requests for optimistic concurrency.",
"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.",
"minimum": 1
},
"currency": {
Expand Down Expand Up @@ -54506,7 +54509,7 @@
},
"revision": {
"type": "integer",
"description": "Revision number after this update. Use this value in subsequent update_media_buy requests for optimistic concurrency.",
"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.",
"minimum": 1
},
"currency": {
Expand Down Expand Up @@ -104973,4 +104976,4 @@
"generatedAt": "2026-05-26T03:04:14.411Z",
"note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1914,9 +1914,12 @@
"$ref": "#/$defs/MediaBuyStatus"
},
"confirmed_at": {
"type": "string",
"type": [
"string",
"null"
],
"format": "date-time",
"description": "ISO 8601 timestamp when this media buy was confirmed by the seller. A successful create_media_buy response constitutes order confirmation."
"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."
},
"creative_deadline": {
"type": "string",
Expand All @@ -1925,7 +1928,7 @@
},
"revision": {
"type": "integer",
"description": "Initial revision number for this media buy. Use in subsequent update_media_buy requests for optimistic concurrency.",
"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.",
"minimum": 1
},
"currency": {
Expand Down Expand Up @@ -9976,4 +9979,4 @@
"generatedAt": "2026-05-26T03:04:14.740Z",
"note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@
},
"revision": {
"type": "integer",
"description": "Revision number after this update. Use this value in subsequent update_media_buy requests for optimistic concurrency.",
"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.",
"minimum": 1
},
"currency": {
Expand Down Expand Up @@ -7609,4 +7609,4 @@
"generatedAt": "2026-05-26T03:04:14.907Z",
"note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@
"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."
},
"confirmed_at": {
"type": "string",
"type": [
"string",
"null"
],
"format": "date-time",
"description": "ISO 8601 timestamp when this media buy was confirmed by the seller. A successful create_media_buy response constitutes order confirmation."
"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."
},
"creative_deadline": {
"type": "string",
Expand All @@ -51,7 +54,7 @@
},
"revision": {
"type": "integer",
"description": "Initial revision number for this media buy. Use in subsequent update_media_buy requests for optimistic concurrency.",
"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.",
"minimum": 1
},
"currency": {
Expand Down Expand Up @@ -222,4 +225,4 @@
}
],
"properties": {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,12 @@
"description": "ISO 8601 timestamp for creative upload deadline"
},
"confirmed_at": {
"type": "string",
"type": [
"string",
"null"
],
"format": "date-time",
"description": "ISO 8601 timestamp when the seller confirmed this media buy. A successful create_media_buy response constitutes order confirmation."
"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."
},
"cancellation": {
"type": "object",
Expand Down Expand Up @@ -111,7 +114,7 @@
},
"revision": {
"type": "integer",
"description": "Current revision number. Pass this in update_media_buy for optimistic concurrency.",
"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.",
"minimum": 1
},
"created_at": {
Expand Down Expand Up @@ -434,4 +437,4 @@
"media_buys"
],
"additionalProperties": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
},
"revision": {
"type": "integer",
"description": "Revision number after this update. Use this value in subsequent update_media_buy requests for optimistic concurrency.",
"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.",
"minimum": 1
},
"currency": {
Expand Down Expand Up @@ -202,4 +202,4 @@
}
],
"properties": {}
}
}
8 changes: 8 additions & 0 deletions src/adcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
verify_agent_authorization,
verify_agent_for_property,
)
from adcp.canonical_formats import (
format_is_supported,
formats_are_equivalent,
upgrade_legacy_format_id,
)
from adcp.capabilities import ( # noqa: F401
FeatureResolver,
build_synthetic_capabilities,
Expand Down Expand Up @@ -819,6 +824,9 @@ def get_adcp_version() -> str:
"Error",
"Format",
"FormatId",
"format_is_supported",
"formats_are_equivalent",
"upgrade_legacy_format_id",
"FormatOptionReference",
"AssetContentType",
"Product",
Expand Down
Loading
Loading