You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Sales agents and signals agents publish catalogs (get_products buying_mode: \"wholesale\", and the proposed get_signals discovery_mode: \"wholesale\" in #4762) that consumers want to mirror locally. Today, the only way to detect catalog changes is to re-fetch the entire catalog and diff. This produces three concrete problems:
Cost and latency. A storefront syncing N sources hits each get_products paginated catalog every poll interval, even when nothing has changed. Sellers absorb the load; consumers pay the latency.
No fast-path for "just changed." A seller who just updated a bundle's pricing has no way to tell consumers "re-fetch me now." Consumers see the change on the next polling interval — minutes to hours later — which is unacceptable for time-sensitive pricing changes (dayparting, makegood adjustments).
No diff signal at all. Even with full re-fetch, consumers must compare every product/signal field against a local snapshot to detect changes. There is no protocol-level "this product changed since you last saw it" primitive.
The registry already has a solved version of this problem in specs/registry-change-feed.md: UUID-v7 cursor-based event feed, optional webhook notifications, retention window, denormalized payloads. That spec covers properties, agents, publishers, and authorizations at the registry level. The gap is the analogous mechanism at the agent level — for products and signals inside a single sales/signals agent's catalog.
Goals
Consumers can poll a single per-agent feed endpoint and maintain a near-real-time mirror of the agent's product and signal catalog without re-fetching unchanged inventory.
Sellers can trigger immediate notification to subscribers after a catalog mutation.
Optional webhook subscriptions reduce polling frequency, with delivery semantics consistent with the registry feed.
The mechanism is symmetric for sales agents (products) and signals agents (signals). Agents that are both publish both event families on one feed.
Polling is the source of truth. Webhooks are best-effort notifications; the feed is durable. Same posture as the registry feed.
Cursor-based, not timestamp-based. UUID v7 event IDs are monotonically ordered and avoid clock-skew problems.
Events are denormalized. Payload contains the post-change state of the entity, so consumers can update local state without a follow-up get_products/get_signals call.
One feed per agent. A sales agent that also publishes signals exposes both event families on one feed. Consumers filter by event type.
Symmetric with the registry feed. A consumer that already implements RegistrySync should be able to implement CatalogSync against an agent with minimal new code.
Event Model
Event Types
Event
Trigger
Why consumers care
product.created
New product added to the agent's catalog
New inventory available for composition / discovery
Pricing options changed (new option, removed option, price/floor change)
Composition layer must re-price; existing media buys unaffected (locked at create_media_buy time)
product.removed
Product no longer available
Remove from catalog; existing media buys honored per cancellation policy
signal.created
New signal added
New targeting/composition option
signal.updated
Signal metadata changed (description, coverage, deployments)
Re-render in consumer catalog
signal.priced
Signal pricing options changed
Composition layer must re-price
signal.removed
Signal no longer available
Remove from catalog
catalog.bulk_change
Agent performed a bulk operation (e.g., seasonal rate-card update affecting >100 entities)
Trigger consumer re-sync via wholesale enumeration rather than processing every event
catalog.bulk_change is the "fast-forward" event. When a seller does a rate-card sweep that touches thousands of products, the agent SHOULD emit one catalog.bulk_change event with a summary payload rather than thousands of product.priced events.
The post-change pricing_options[] is included in full. previous_pricing_option_ids[] lets consumers detect that po_cpm_v1 was retired. effective_at lets the agent announce changes before they take effect.
The change-feed endpoints live on the agent itself, not a central registry.
GET /catalog/events
Poll the change feed.
Authentication: Required. The caller must be authorized to call get_products / get_signals in wholesale mode against this agent — same authorization scope.
Parameters:
Param
Type
Default
Description
cursor
UUID
(none)
Last event_id processed. Omit for start of retention window.
Retention is agent-declared (recommend 30 days minimum). Consumers whose cursor is older than the retention window get a RETENTION_EXPIRED error and MUST resync via wholesale enumeration.
POST /catalog/subscriptions
Register a webhook for change notifications. Optional — agents MAY refuse to implement webhooks and require polling.
Agents that don't declare this stanza are presumed to not support the feed. Consumers fall back to polling via wholesale mode (optionally with catalog_version probes from #4761).
Consumer Pattern
Bootstrap: Call get_products buying_mode: \"wholesale\" (and/or get_signals discovery_mode: \"wholesale\") — paginated full enumeration. Persist locally with entity IDs.
Steady state: Poll GET /catalog/events?cursor={last_event_id} every 30–60 seconds, or wait for webhook notification and then poll. Apply events to local catalog.
Recovery: If next_cursor returns RETENTION_EXPIRED or a catalog.bulk_change event is observed, re-bootstrap via wholesale.
Relationship to Other Specs
specs/registry-change-feed.md covers the central registry (properties, agents, publishers, authorizations). This spec covers per-agent inventory (products, signals). They compose: an agent.profile_updated event in the registry feed indicates a coarse change at the agent level; the agent's own catalog feed gives entity-level detail.
Event log + feed endpoint. Reference implementation in the AdCP signals/sales agent SDKs. catalog_events storage, GET /catalog/events endpoint, capability declaration. Solves polling-based change detection.
SDK CatalogSync client. Add CatalogSync to @adcp/client (TypeScript first, then Go and Python). Mirrors RegistrySync: bootstrap via wholesale, poll the feed, maintain in-memory product/signal index, event emitter for reactivity.
Webhook subscriptions. Subscription CRUD, delivery worker with coalescing, retry/suspension logic. Most operationally complex — ship after the feed endpoint has proven stable.
Cross-feed correlation. SDK convenience: CatalogSync and RegistrySync together expose authorization-aware views ("Which agents publish signals authorized by this data provider?" answered locally from registry feed + catalog feed without server calls).
Open Questions
Per-agent versus federated feed. An alternative design is a central change-feed at the registry that proxies per-agent catalogs. Rejected: the registry doesn't see inside agent catalogs; agents own their inventory.
Required retention. 30 days is a recommendation. Should the spec MUST a minimum? Recommendation: SHOULD 30 days, MUST not less than 7.
Event ordering across entity types. Strict per-entity ordering is required (product price changes must be linearizable per product_id). Cross-entity ordering is not required — UUID v7 gives consumers a stable cursor without expensive global ordering.
Signing of events. Should events be content-signed by the agent? The registry feed spec defers this to the 4.0 root-of-trust work. Same answer here — out of scope for v3.1.
Happy to follow up with a PR adding this to specs/ if maintainers are aligned on the direction. Reference implementation will land in the prebid salesagent as part of our v3.1 conformance prep.
Problem
Sales agents and signals agents publish catalogs (
get_products buying_mode: \"wholesale\", and the proposedget_signals discovery_mode: \"wholesale\"in #4762) that consumers want to mirror locally. Today, the only way to detect catalog changes is to re-fetch the entire catalog and diff. This produces three concrete problems:get_productspaginated catalog every poll interval, even when nothing has changed. Sellers absorb the load; consumers pay the latency.The registry already has a solved version of this problem in
specs/registry-change-feed.md: UUID-v7 cursor-based event feed, optional webhook notifications, retention window, denormalized payloads. That spec covers properties, agents, publishers, and authorizations at the registry level. The gap is the analogous mechanism at the agent level — for products and signals inside a single sales/signals agent's catalog.Goals
wholesalepolling, optionally withcatalog_versionprobes from Proposal: catalog_version token for conditional catalog fetches (ETag-style) #4761.Design Principles
get_products/get_signalscall.RegistrySyncshould be able to implementCatalogSyncagainst an agent with minimal new code.Event Model
Event Types
product.createdproduct.updatedproduct.pricedproduct.removedsignal.createdsignal.updatedsignal.pricedsignal.removedcatalog.bulk_changecatalog.bulk_changeis the "fast-forward" event. When a seller does a rate-card sweep that touches thousands of products, the agent SHOULD emit onecatalog.bulk_changeevent with a summary payload rather than thousands ofproduct.pricedevents.Event Payload Examples
product.priced:{ \"product_id\": \"prod_premium_ctv_us\", \"pricing_options\": [ { \"pricing_option_id\": \"po_cpm_v2\", \"model\": \"cpm\", \"cpm\": 18.50, \"currency\": \"USD\" } ], \"previous_pricing_option_ids\": [\"po_cpm_v1\"], \"effective_at\": \"2026-06-01T00:00:00Z\" }The post-change
pricing_options[]is included in full.previous_pricing_option_ids[]lets consumers detect thatpo_cpm_v1was retired.effective_atlets the agent announce changes before they take effect.product.updated:{ \"product_id\": \"prod_premium_ctv_us\", \"changed_fields\": [\"format_ids\", \"performance_standards\"], \"product\": { \"...full Product object...\" } }changed_fields[]is advisory — consumers MAY use it for fine-grained re-render, but MUST be able to handle a full replacement of the entity.catalog.bulk_change:{ \"summary\": \"Q3 2026 rate card refresh\", \"affected_entity_types\": [\"product\"], \"affected_count\": 1480, \"recommendation\": \"wholesale_resync\" }API Endpoints
The change-feed endpoints live on the agent itself, not a central registry.
GET /catalog/eventsPoll the change feed.
Authentication: Required. The caller must be authorized to call
get_products/get_signalsinwholesalemode against this agent — same authorization scope.Parameters:
cursorevent_idprocessed. Omit for start of retention window.typesproduct.*limitResponse:
{ \"events\": [ { \"event_id\": \"019539a0-...\", \"event_type\": \"product.priced\", \"entity_type\": \"product\", \"entity_id\": \"prod_premium_ctv_us\", \"payload\": { \"...\" : \"...\" }, \"created_at\": \"2026-05-18T10:00:00Z\" } ], \"next_cursor\": \"019539a1-...\", \"has_more\": true, \"retention_window_days\": 30 }Retention is agent-declared (recommend 30 days minimum). Consumers whose
cursoris older than the retention window get aRETENTION_EXPIREDerror and MUST resync via wholesale enumeration.POST /catalog/subscriptionsRegister a webhook for change notifications. Optional — agents MAY refuse to implement webhooks and require polling.
Request:
{ \"url\": \"https://storefront.example.com/hooks/catalog\", \"events\": [\"product.*\", \"signal.priced\"], \"secret\": \"subscriber-provided-hmac-secret\" }Additional CRUD:
GET /catalog/subscriptions,DELETE /catalog/subscriptions/:id.Webhook Delivery
Webhooks are notifications, not event delivery — same posture as the registry feed.
Coalescing: Events are batched per subscriber per 30-second window.
Retries: 3 attempts with exponential backoff (30s, 5m, 30m). 24 hours of failures → subscription marked
suspended.Capability Declaration
Agents declare feed support in
get_adcp_capabilities:{ \"catalog_change_feed\": { \"supported\": true, \"retention_window_days\": 30, \"webhooks_supported\": true, \"event_types\": [ \"product.created\", \"product.updated\", \"product.priced\", \"product.removed\", \"signal.created\", \"signal.updated\", \"signal.priced\", \"signal.removed\", \"catalog.bulk_change\" ] } }Agents that don't declare this stanza are presumed to not support the feed. Consumers fall back to polling via
wholesalemode (optionally withcatalog_versionprobes from #4761).Consumer Pattern
get_products buying_mode: \"wholesale\"(and/orget_signals discovery_mode: \"wholesale\") — paginated full enumeration. Persist locally with entity IDs.GET /catalog/events?cursor={last_event_id}every 30–60 seconds, or wait for webhook notification and then poll. Apply events to local catalog.next_cursorreturnsRETENTION_EXPIREDor acatalog.bulk_changeevent is observed, re-bootstrap via wholesale.Relationship to Other Specs
specs/registry-change-feed.mdcovers the central registry (properties, agents, publishers, authorizations). This spec covers per-agent inventory (products, signals). They compose: anagent.profile_updatedevent in the registry feed indicates a coarse change at the agent level; the agent's own catalog feed gives entity-level detail.get_signalswholesale mode (Proposal: get_signals wholesale discovery mode (symmetric to get_products) #4762) defines the wholesale enumeration mode that bootstraps consumers before they switch to the feed.catalog_version(Proposal: catalog_version token for conditional catalog fetches (ETag-style) #4761) is a complementary cheap-probe mechanism for agents that don't implement the full feed. Consumers MAY usecatalog_versionto validate their cursor is still current without consuming feed bandwidth.Implementation Phases
catalog_eventsstorage,GET /catalog/eventsendpoint, capability declaration. Solves polling-based change detection.CatalogSyncclient. AddCatalogSyncto@adcp/client(TypeScript first, then Go and Python). MirrorsRegistrySync: bootstrap via wholesale, poll the feed, maintain in-memory product/signal index, event emitter for reactivity.CatalogSyncandRegistrySynctogether expose authorization-aware views ("Which agents publish signals authorized by this data provider?" answered locally from registry feed + catalog feed without server calls).Open Questions
product_id). Cross-entity ordering is not required — UUID v7 gives consumers a stable cursor without expensive global ordering.Happy to follow up with a PR adding this to
specs/if maintainers are aligned on the direction. Reference implementation will land in the prebid salesagent as part of our v3.1 conformance prep.