Motivation
An AdCP buyer can connect to an existing seller account that is also operated
through the seller's UI, APIs, automation, and connected platforms. The buyer
must not see only the changes it initiated. Campaigns, packages, creatives,
budgets, account state, and reporting can change without any preceding AdCP
task from that buyer.
Current 3.2 surfaces do not provide a complete answer to either of these
questions:
- What is true now across the seller account?
- What committed AdCP-visible state changed while the buyer was offline?
Authoritative reads answer parts of the first question. Notifications cover
selected lifecycle transitions. webhook_activity[] answers whether a seller
attempted to deliver selected webhooks. get_media_buys.history[] is bounded
per buy. None is a durable, ordered account change feed, and several external
mutation paths have no notification at all.
This is a foundational multi-operator-account requirement, not only an audit
feature. A buyer that reconnects to an established account must be able to
establish a complete baseline, observe seller/platform activity it did not
initiate, and mechanically converge its local projection to the seller's
authoritative state.
Scope
Completeness invariant
For every committed change to account-scoped control-plane state designated by
an authoritative AdCP read, the seller MUST:
- reflect the resulting current state on that read regardless of whether the
change originated through AdCP, a seller UI or API, seller automation,
another authorized principal, or a connected platform;
- append at least one immutable account change record at the applicable seller persistence boundary;
- make the snapshot and record readable before enqueueing notification; and
- emit
account.change_recorded to every active account subscriber that
requested that event type.
For a seller-mediated mutation, the authoritative projection and change records commit atomically. For an externally mediated change, records commit with the seller's ingestion or projection of the observation. A pass-through read MAY lead the feed only within the connected source's declared freshness bound; no cross-platform transaction is required. The task response that acknowledged a synchronous AdCP mutation does not replace the account notification. Other authorized principals may share the same account and need the same change stream.
The minimum granularity is one record per independently repairable authoritative identity. Nested package or assignment changes MAY coalesce under a media-buy record only when get_media_buys repairs the complete changed closure. Records from one commit MAY share batch_id, but retain independent change IDs, cursor positions, and notifications.
The invariant covers committed control-plane changes, including resource
creation/discovery, lifecycle, spend and delivery controls, flight, targeting,
creative content and assignment, account access/status, financial state,
reporting corrections/finality, and deletion or purge markers.
It does not create activity for reads, validation failures, dry runs, exact
idempotency replays, no-op observations, webhook attempts, impression-level
metric accumulation, raw audience members, raw logged conversion events, or
internal optimization decisions that remain within already-authorized policy.
Those exclusions prevent the management-plane feed from becoming a data-plane
firehose or a copy of sensitive payloads.
Origin-neutral authoritative reads
The RFC standardizes the existing strong get_media_buys principle across all
covered reads: account-visible state is included because the caller can access
it, not because it was created through AdCP.
The implementation PR MUST include a normative coverage matrix for at least:
| Resource family |
Authoritative current-state read |
Required 3.2 disposition |
| Account identity, status, authorization, billing configuration |
list_accounts |
Complete current state plus change records |
| Account spend, credit, payment, invoices |
get_account_financials |
Complete when the capability is declared |
| Media buys and packages |
get_media_buys |
All account-visible buys, including externally created or modified buys |
| Media-buy delivery and reporting corrections |
get_media_buy_delivery |
Declared window parity; corrections and finality revisions recorded |
| Creative library |
list_creatives |
All account-visible creatives, source-agnostic content revision/digest and lifecycle state |
| Creative assignments and approval |
get_media_buys; optional reverse projection on list_creatives |
Exact current relationship and change records |
| Audiences |
sync_audiences discovery mode |
Explicit native/connected resource identity, management origin, and revision or a documented 3.2 exclusion |
| Event sources |
sync_event_sources discovery mode |
Buyer- and seller-managed resources plus change records |
| Catalogs |
sync_catalogs discovery mode |
Explicit native/connected resource identity, management origin, and revision or a documented 3.2 exclusion |
| Wholesale products and signals |
list_products / get_signals |
Existing versioned feed remains authoritative; account feed records control/bulk changes without duplicating every data-plane body |
Inline creative state, native audiences/catalogs, and any mutable package fields
not currently recoverable through a read are specification gaps, not reasons to
weaken the completeness claim. They must be made readable or explicitly
excluded from the 3.2 claim before ratification.
New task: list_account_changes
This is an account-scoped, read-only, cursor-resumable feed. It is distinct
from current-state snapshots, task history, and webhook transport diagnostics.
Illustrative request:
{
"account": { "account_id": "acc_streamhaus_pinnacle" },
"starting_position": "latest",
"max_results": 100
}
starting_position is valid only without cursor:
earliest intentionally starts at the oldest retained change and is the completeness-first default.
latest returns a checkpoint at the current high-water mark for race-free
baseline bootstrap.
Illustrative response:
{
"changes": [],
"cursor": "opaque-account-principal-checkpoint",
"has_more": false,
"available_since": "2026-05-26T00:00:00Z",
"generated_at": "2026-08-24T12:00:00Z"
}
Unlike ordinary list pagination, cursor is always returned, including on an
empty tail response. It is scoped to authenticated principal, authorization
scope epoch, account, and normalized filters. It denotes "strictly after" the
scanned high-water mark; timestamps never determine ordering. Appends never
reorder prior pages. A principal/account/filter mismatch MUST return
INVALID_REQUEST at cursor; sellers MUST NOT silently apply a new scope at
the embedded position. A visibility change rotates the authorization epoch and
returns CURSOR_EXPIRED with details.reason: "authorization_scope_changed",
forcing snapshot rebootstrap.
An expired cursor MUST return CURSOR_EXPIRED with recovery guidance to obtain
a new latest checkpoint, rebuild authoritative snapshots, and drain from that
checkpoint. It MUST NOT silently restart from the retention boundary.
The feed retains changes for at least 90 days. available_since states the
actual retained boundary. The operational feed is not a substitute for a
buyer's regulatory archive.
Account change record
Illustrative shape:
{
"change_id": "chg_019c9f4f4d2e7b89",
"recorded_at": "2026-08-24T11:58:04Z",
"occurred_at": "2026-08-24T11:57:41Z",
"resource": {
"type": "creative",
"account_id": "acc_streamhaus_pinnacle",
"resource_id": "cr_8421"
},
"action": "updated",
"origin": {
"kind": "connected_platform",
"connection_id": "conn_social_primary"
},
"resource_revision": 12,
"changed_paths": ["/name", "/assets/video"],
"repair": {
"task": "list_creatives"
}
}
Required fields:
- stable
change_id;
- seller
recorded_at;
- structured resource identity;
- standard action (
created, discovered, updated, status_changed,
linked, unlinked, deleted, or a future namespaced action);
- server-derived origin class;
- an allowlisted authoritative repair task. The receiver constructs and validates request arguments locally from the authenticated account and structured resource identity; feed-provided arguments are never dispatched.
Optional fields include trustworthy upstream occurred_at, post-change
resource revision, changed JSON Pointer paths, privacy-safe actor, categorical
reason, summary, and bounded extension metadata. Records do not carry secrets,
credentials, bank details, raw audience members/events, webhook bodies, or
unbounded before/after snapshots. Large or sensitive values remain on the
authorized repair read.
Deletions retain a tombstone record. A compelled legal hard purge may retain
only non-sensitive resource identity, time, purge category, and repair
disposition.
Notification: account.change_recorded
The new account-anchored type is registered through existing
sync_accounts.accounts[].notification_configs[].
Each change record produces one logical notification per subscribed endpoint.
The notification is a compact invalidation containing account_id,
change_id, resource identity, action, recorded_at, and an advisory feed
high-water mark. notification_id equals change_id. Transport retries reuse
the same idempotency_key; deliberate re-emission uses a new delivery key with
the same logical notification ID.
The payload is not authoritative state and the high-water mark is not a cursor
the buyer may install without reading. Receivers drain from their persisted
cursor, then reread the named resource. Existing specialized notifications
remain valid and may duplicate the generic change notification.
Capability
Add account.change_feed as a discriminated capability. When supported it
declares:
{
"supported": true,
"read_task": "list_account_changes",
"registration_task": "sync_accounts",
"event_type": "account.change_recorded",
"retention_days": 90,
"resource_types": ["account", "media_buy", "package", "creative", "creative_assignment", "delivery"]
}
The seller-wide capability describes possible coverage. Each account-feed
response additionally reports account-specific source coverage and connector
freshness. A connected source reporting current supplies
last_successful_sync_at and stale_after_seconds; at response generation the
last successful sync is within that bound and no ingestion gap is known.
has_more: false means caught up to the seller's ingestion boundary, not
necessarily to an unreachable upstream platform.
Snapshot completeness itself is normative and is not weakened into an
optional complete: true self-assertion. A seller may declare a connected
source delayed or unavailable; it may not claim complete current state while
silently omitting resources it can access through that connection.
Race-free buyer workflow
- Discover
account.change_feed and account-specific coverage.
- Register
account.change_recorded before relying on push.
- Call
list_account_changes(starting_position: "latest") and persist C0.
- Enumerate every authoritative account snapshot, including all lifecycle
statuses rather than active-only defaults.
- Drain account changes strictly after C0 and reread affected resources.
- On each signed notification, drain again from the persisted cursor.
- Poll periodically so webhook loss does not create a gap.
- On
CURSOR_EXPIRED or a declared connector coverage gap, repeat bootstrap.
Conformance and training
The 3.2 implementation includes capability-gated conformance for:
- source-agnostic external create, update, status, relationship, financial,
reporting-correction, and delete/purge changes;
- atomic snapshot + change record visibility before webhook enqueue;
- one record and notification for AdCP, seller UI, connected-platform,
seller-system, and other-principal mutations;
- no record for failed/no-op/dry-run/idempotent replay operations;
- cursor ordering, pagination, same-timestamp changes, concurrent append,
cursor-scope mismatch rejection, empty-tail checkpoints, authorization-epoch
changes, expiry, and rebootstrap;
- subscriber activation, fan-out, retry/re-emission identity, and coexistence
with specialized notifications;
- cross-account/principal isolation, actor redaction, and secret/PII absence;
- advertised retention and connected-source freshness.
The public training seller adds an existing, non-exclusively managed sandbox
account. The initial reference scenario uses a connected-platform simulator to add
and later modify a creative outside the learner's AdCP creative calls. The
seller advertises only creative coverage until campaign, budget, assignment,
financial, and reporting paths pass the same completeness tests. The learner
must subscribe, bootstrap snapshots, consume signed notifications, drain the
change feed, and reconcile without treating the webhook payload as current
truth. Training and certification material must teach this workflow on both
buyer and seller/platform tracks.
Alternatives considered
Strengthen snapshots and add notifications, but no durable feed
This is sufficient for eventual current-state convergence only if a buyer can
afford complete scans. It cannot answer what changed, preserve deletion
markers, close bootstrap races efficiently, or recover notification metadata
after an outage.
Reuse webhook_activity[]
Rejected. That surface records delivery attempts to one calling principal's
endpoint, is bounded and non-cursorable, and answers "did the seller fire?"
rather than "what business state changed?"
Reuse get_media_buys.history[] or task history
Rejected. Media-buy history is bounded per resource and does not cover account,
creative, financial, reporting, audience, catalog, or connected-platform
changes. Task history covers protocol operations, not external mutations.
Add one webhook type for every missing resource transition
Rejected as the completeness mechanism. Specialized notifications remain
useful, but webhook-only history has no durable cursor recovery and causes the
closed event-type registry to grow for every new resource. One open, typed
account change record plus a generic notification is the smaller complete
surface.
Call the task list_account_activity
Considered, but activity is broader than the proposed contract and collides
with webhook_activity. list_account_changes makes clear that reads, failed
attempts, logins, and transport retries are not included. A future security or
operator audit surface may legitimately cover those broader activities.
Compatibility impact
This is an additive but normative AdCP 3.2 change:
- new optional
list_account_changes task;
- new request, response, change-record, and webhook schemas;
- new
account.change_recorded notification enum value;
- new
account.change_feed capability;
- new
CURSOR_EXPIRED error use/definition if the existing code cannot be
reused cleanly;
- normative source-neutral coverage requirements for advertised resource
families;
- schema registry, docs, compliance, SDK, training-agent, and certification
updates.
Older clients receive no new event implicitly. Sellers emit
account.change_recorded only after 3.2 negotiation and explicit subscription.
The task is advertised only when supported. No stable 3.1 backport is proposed.
Reviewer checklist
Motivation
An AdCP buyer can connect to an existing seller account that is also operated
through the seller's UI, APIs, automation, and connected platforms. The buyer
must not see only the changes it initiated. Campaigns, packages, creatives,
budgets, account state, and reporting can change without any preceding AdCP
task from that buyer.
Current 3.2 surfaces do not provide a complete answer to either of these
questions:
Authoritative reads answer parts of the first question. Notifications cover
selected lifecycle transitions.
webhook_activity[]answers whether a sellerattempted to deliver selected webhooks.
get_media_buys.history[]is boundedper buy. None is a durable, ordered account change feed, and several external
mutation paths have no notification at all.
This is a foundational multi-operator-account requirement, not only an audit
feature. A buyer that reconnects to an established account must be able to
establish a complete baseline, observe seller/platform activity it did not
initiate, and mechanically converge its local projection to the seller's
authoritative state.
Scope
Completeness invariant
For every committed change to account-scoped control-plane state designated by
an authoritative AdCP read, the seller MUST:
change originated through AdCP, a seller UI or API, seller automation,
another authorized principal, or a connected platform;
account.change_recordedto every active account subscriber thatrequested that event type.
For a seller-mediated mutation, the authoritative projection and change records commit atomically. For an externally mediated change, records commit with the seller's ingestion or projection of the observation. A pass-through read MAY lead the feed only within the connected source's declared freshness bound; no cross-platform transaction is required. The task response that acknowledged a synchronous AdCP mutation does not replace the account notification. Other authorized principals may share the same account and need the same change stream.
The minimum granularity is one record per independently repairable authoritative identity. Nested package or assignment changes MAY coalesce under a media-buy record only when
get_media_buysrepairs the complete changed closure. Records from one commit MAY sharebatch_id, but retain independent change IDs, cursor positions, and notifications.The invariant covers committed control-plane changes, including resource
creation/discovery, lifecycle, spend and delivery controls, flight, targeting,
creative content and assignment, account access/status, financial state,
reporting corrections/finality, and deletion or purge markers.
It does not create activity for reads, validation failures, dry runs, exact
idempotency replays, no-op observations, webhook attempts, impression-level
metric accumulation, raw audience members, raw logged conversion events, or
internal optimization decisions that remain within already-authorized policy.
Those exclusions prevent the management-plane feed from becoming a data-plane
firehose or a copy of sensitive payloads.
Origin-neutral authoritative reads
The RFC standardizes the existing strong
get_media_buysprinciple across allcovered reads: account-visible state is included because the caller can access
it, not because it was created through AdCP.
The implementation PR MUST include a normative coverage matrix for at least:
list_accountsget_account_financialsget_media_buysget_media_buy_deliverylist_creativesget_media_buys; optional reverse projection onlist_creativessync_audiencesdiscovery modesync_event_sourcesdiscovery modesync_catalogsdiscovery modelist_products/get_signalsInline creative state, native audiences/catalogs, and any mutable package fields
not currently recoverable through a read are specification gaps, not reasons to
weaken the completeness claim. They must be made readable or explicitly
excluded from the 3.2 claim before ratification.
New task:
list_account_changesThis is an account-scoped, read-only, cursor-resumable feed. It is distinct
from current-state snapshots, task history, and webhook transport diagnostics.
Illustrative request:
{ "account": { "account_id": "acc_streamhaus_pinnacle" }, "starting_position": "latest", "max_results": 100 }starting_positionis valid only withoutcursor:earliestintentionally starts at the oldest retained change and is the completeness-first default.latestreturns a checkpoint at the current high-water mark for race-freebaseline bootstrap.
Illustrative response:
{ "changes": [], "cursor": "opaque-account-principal-checkpoint", "has_more": false, "available_since": "2026-05-26T00:00:00Z", "generated_at": "2026-08-24T12:00:00Z" }Unlike ordinary list pagination,
cursoris always returned, including on anempty tail response. It is scoped to authenticated principal, authorization
scope epoch, account, and normalized filters. It denotes "strictly after" the
scanned high-water mark; timestamps never determine ordering. Appends never
reorder prior pages. A principal/account/filter mismatch MUST return
INVALID_REQUESTatcursor; sellers MUST NOT silently apply a new scope atthe embedded position. A visibility change rotates the authorization epoch and
returns
CURSOR_EXPIREDwithdetails.reason: "authorization_scope_changed",forcing snapshot rebootstrap.
An expired cursor MUST return
CURSOR_EXPIREDwith recovery guidance to obtaina new latest checkpoint, rebuild authoritative snapshots, and drain from that
checkpoint. It MUST NOT silently restart from the retention boundary.
The feed retains changes for at least 90 days.
available_sincestates theactual retained boundary. The operational feed is not a substitute for a
buyer's regulatory archive.
Account change record
Illustrative shape:
{ "change_id": "chg_019c9f4f4d2e7b89", "recorded_at": "2026-08-24T11:58:04Z", "occurred_at": "2026-08-24T11:57:41Z", "resource": { "type": "creative", "account_id": "acc_streamhaus_pinnacle", "resource_id": "cr_8421" }, "action": "updated", "origin": { "kind": "connected_platform", "connection_id": "conn_social_primary" }, "resource_revision": 12, "changed_paths": ["/name", "/assets/video"], "repair": { "task": "list_creatives" } }Required fields:
change_id;recorded_at;created,discovered,updated,status_changed,linked,unlinked,deleted, or a future namespaced action);Optional fields include trustworthy upstream
occurred_at, post-changeresource revision, changed JSON Pointer paths, privacy-safe actor, categorical
reason, summary, and bounded extension metadata. Records do not carry secrets,
credentials, bank details, raw audience members/events, webhook bodies, or
unbounded before/after snapshots. Large or sensitive values remain on the
authorized repair read.
Deletions retain a tombstone record. A compelled legal hard purge may retain
only non-sensitive resource identity, time, purge category, and repair
disposition.
Notification:
account.change_recordedThe new account-anchored type is registered through existing
sync_accounts.accounts[].notification_configs[].Each change record produces one logical notification per subscribed endpoint.
The notification is a compact invalidation containing
account_id,change_id, resource identity, action,recorded_at, and an advisory feedhigh-water mark.
notification_idequalschange_id. Transport retries reusethe same
idempotency_key; deliberate re-emission uses a new delivery key withthe same logical notification ID.
The payload is not authoritative state and the high-water mark is not a cursor
the buyer may install without reading. Receivers drain from their persisted
cursor, then reread the named resource. Existing specialized notifications
remain valid and may duplicate the generic change notification.
Capability
Add
account.change_feedas a discriminated capability. When supported itdeclares:
{ "supported": true, "read_task": "list_account_changes", "registration_task": "sync_accounts", "event_type": "account.change_recorded", "retention_days": 90, "resource_types": ["account", "media_buy", "package", "creative", "creative_assignment", "delivery"] }The seller-wide capability describes possible coverage. Each account-feed
response additionally reports account-specific source coverage and connector
freshness. A connected source reporting
currentsupplieslast_successful_sync_atandstale_after_seconds; at response generation thelast successful sync is within that bound and no ingestion gap is known.
has_more: falsemeans caught up to the seller's ingestion boundary, notnecessarily to an unreachable upstream platform.
Snapshot completeness itself is normative and is not weakened into an
optional
complete: trueself-assertion. A seller may declare a connectedsource delayed or unavailable; it may not claim complete current state while
silently omitting resources it can access through that connection.
Race-free buyer workflow
account.change_feedand account-specific coverage.account.change_recordedbefore relying on push.list_account_changes(starting_position: "latest")and persist C0.statuses rather than active-only defaults.
CURSOR_EXPIREDor a declared connector coverage gap, repeat bootstrap.Conformance and training
The 3.2 implementation includes capability-gated conformance for:
reporting-correction, and delete/purge changes;
seller-system, and other-principal mutations;
cursor-scope mismatch rejection, empty-tail checkpoints, authorization-epoch
changes, expiry, and rebootstrap;
with specialized notifications;
The public training seller adds an existing, non-exclusively managed sandbox
account. The initial reference scenario uses a connected-platform simulator to add
and later modify a creative outside the learner's AdCP creative calls. The
seller advertises only
creativecoverage until campaign, budget, assignment,financial, and reporting paths pass the same completeness tests. The learner
must subscribe, bootstrap snapshots, consume signed notifications, drain the
change feed, and reconcile without treating the webhook payload as current
truth. Training and certification material must teach this workflow on both
buyer and seller/platform tracks.
Alternatives considered
Strengthen snapshots and add notifications, but no durable feed
This is sufficient for eventual current-state convergence only if a buyer can
afford complete scans. It cannot answer what changed, preserve deletion
markers, close bootstrap races efficiently, or recover notification metadata
after an outage.
Reuse
webhook_activity[]Rejected. That surface records delivery attempts to one calling principal's
endpoint, is bounded and non-cursorable, and answers "did the seller fire?"
rather than "what business state changed?"
Reuse
get_media_buys.history[]or task historyRejected. Media-buy history is bounded per resource and does not cover account,
creative, financial, reporting, audience, catalog, or connected-platform
changes. Task history covers protocol operations, not external mutations.
Add one webhook type for every missing resource transition
Rejected as the completeness mechanism. Specialized notifications remain
useful, but webhook-only history has no durable cursor recovery and causes the
closed event-type registry to grow for every new resource. One open, typed
account change record plus a generic notification is the smaller complete
surface.
Call the task
list_account_activityConsidered, but
activityis broader than the proposed contract and collideswith
webhook_activity.list_account_changesmakes clear that reads, failedattempts, logins, and transport retries are not included. A future security or
operator audit surface may legitimately cover those broader activities.
Compatibility impact
This is an additive but normative AdCP 3.2 change:
list_account_changestask;account.change_recordednotification enum value;account.change_feedcapability;CURSOR_EXPIREDerror use/definition if the existing code cannot bereused cleanly;
families;
updates.
Older clients receive no new event implicitly. Sellers emit
account.change_recordedonly after 3.2 negotiation and explicit subscription.The task is advertised only when supported. No stable 3.1 backport is proposed.
Reviewer checklist