Skip to content

RFC: complete account change visibility across seller-managed state #6810

Description

@bokelley

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:

  1. What is true now across the seller account?
  2. 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:

  1. 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;
  2. append at least one immutable account change record at the applicable seller persistence boundary;
  3. make the snapshot and record readable before enqueueing notification; and
  4. 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

  1. Discover account.change_feed and account-specific coverage.
  2. Register account.change_recorded before relying on push.
  3. Call list_account_changes(starting_position: "latest") and persist C0.
  4. Enumerate every authoritative account snapshot, including all lifecycle
    statuses rather than active-only defaults.
  5. Drain account changes strictly after C0 and reread affected resources.
  6. On each signed notification, drain again from the persisted cursor.
  7. Poll periodically so webhook loss does not create a gap.
  8. 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 is clear and not redundant with existing functionality
  • Scope is specific enough to implement without further clarification
  • Alternatives section covers at least one non-obvious alternative
  • Compatibility impact accurately states breaking vs. non-breaking
  • Wire-format or schema snippet included

Metadata

Metadata

Assignees

No one assigned

    Labels

    claude-triagedIssue has been triaged by the Claude Code triage routine. Remove to re-triage.creativeepicMajor deliverable — auto-adds to roadmap boardmedia-buyIssue concerns the media-buy protocol domainrfcProtocol change — auto-adds to roadmap boardspec / protocol

    Type

    No type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions