Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions .changeset/add-account-change-feed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"adcontextprotocol": minor
---

Add the AdCP 3.2 draft account change feed from RFC #6810: source-neutral authoritative reads, `list_account_changes`, `account.change_recorded`, account-specific connected-source coverage, 90-day retention, explicit cursor-expiry recovery, capability-gated conformance, and a shared-account training lab. The draft remains blocked on RFC ratification before merge.
2 changes: 2 additions & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@
"docs/accounts/provisioning-walkthrough",
"docs/accounts/tasks/sync_accounts",
"docs/accounts/tasks/list_accounts",
"docs/accounts/tasks/list_account_changes",
"docs/accounts/tasks/report_usage",
"docs/accounts/tasks/sync_governance",
"docs/accounts/tasks/get_account_financials"
Expand Down Expand Up @@ -679,6 +680,7 @@
"docs/learning/overview",
"docs/learning/instructional-design",
"docs/learning/failure-mode-scope",
"docs/learning/shared-account-change-feed",
{
"group": "Basics (free)",
"pages": [
Expand Down
150 changes: 150 additions & 0 deletions docs/accounts/tasks/list_account_changes.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
---
title: list_account_changes
description: "Read the durable ordered feed of material changes to authoritative state on one shared advertiser account."
"og:title": "AdCP — list_account_changes"
testable: false
---

`list_account_changes` returns material changes to AdCP-visible state on one
account, including changes made outside the observing buyer's calls. Use it
after a snapshot bootstrap, after receiving `account.change_recorded`, or for
an account-level audit of state transitions.

<Warning>
This is a 3.2 implementation draft for [RFC #6810](https://github.com/adcontextprotocol/adcp/issues/6810). It is not normative until the RFC is ratified.
</Warning>

The feed is optional and appears only when
`get_adcp_capabilities.account.change_feed.supported` is `true`.

## What the feed covers

For every resource type the seller advertises, a material committed change
must both appear here and be reflected on its authoritative read, regardless
of whether it originated through AdCP, a seller operator or system, another
authorized principal, or a connected platform.

The feed describes control-plane changes: creation, configuration, lifecycle,
relationships, reporting corrections, and deletion markers. It excludes
reads, failed operations, dry runs, exact idempotency replays, delivery counter
increments, raw audience members, raw events, catalog item bodies, and webhook
delivery attempts.

For the complete coverage matrix and rationale, see the
[account change feed draft](https://github.com/adcontextprotocol/adcp/blob/main/specs/account-change-feed.md).

## Request

**Request schema:** [`account/list-account-changes-request.json`](https://adcontextprotocol.org/schemas/latest/account/list-account-changes-request.json)

| Field | Required | Description |
| --- | --- | --- |
| `account` | Yes | Exactly one account reference. |
| `cursor` | No | Opaque checkpoint from an earlier call under the same principal, account, and filters. Mutually exclusive with `starting_position`. |
| `starting_position` | No | `earliest` (default) or `latest`. Use `latest` before a snapshot bootstrap. |
| `resource_types` | No | Exact resource-type filter. The cursor is bound to the normalized filter. |
| `max_results` | No | 1–100 changes; default 50. |

Acquire a high-water checkpoint before reading several snapshots:

```json
{
"adcp_version": "3.2",
"account": { "account_id": "acc_luma_shared" },
"starting_position": "latest",
"max_results": 100
}
```

## Response

**Response schema:** [`account/list-account-changes-response.json`](https://adcontextprotocol.org/schemas/latest/account/list-account-changes-response.json)

| Field | Description |
| --- | --- |
| `changes` | Oldest-first material change records. |
| `cursor` | Checkpoint after the scanned high-water. Always present, even on an empty tail page. |
| `has_more` | Whether more retained matching records were available when the page was generated. |
| `available_since` | Current retention boundary for this caller and account. |
| `generated_at` | Seller time for the page and coverage watermarks. |
| `source_coverage` | Optional per-account connected-source status and freshness. |

Each change record names the changed resource, action, server-derived origin,
optional changed paths and revision, and an allowlisted read-only
`repair.task` hint. It deliberately does not include full before/after values.

```json
{
"changes": [
{
"change_id": "chg_01K38G7X8ZGX9T4F1Q5W6Y2M3N",
"recorded_at": "2026-08-24T11:58:04Z",
"resource": {
"type": "creative",
"account_id": "acc_luma_shared",
"resource_id": "cr_8421"
},
"action": "updated",
"origin": {
"kind": "connected_platform",
"connection_id": "conn_social_primary"
},
"changed_paths": ["/name", "/assets/0"],
"repair": { "task": "list_creatives" }
}
],
"cursor": "opaque-checkpoint",
"has_more": false,
"available_since": "2026-05-26T00:00:00Z",
"generated_at": "2026-08-24T12:00:00Z"
}
```

## Cursor contract

Cursor order, not timestamps, defines the total order. Appends never reorder
prior pages. A filtered empty page still advances across scanned nonmatching
records, so persist every returned cursor.

A cursor is scoped to the authenticated principal, resolved account, and
normalized filters. Do not move it between credentials or filter sets.

Sellers retain changes for at least 90 days. An expired cursor returns
[`CURSOR_EXPIRED`](/docs/building/verification/compliance-catalog#error-code-cursor-expired); it never silently restarts. Recovery is to obtain a new
`latest` checkpoint, rebuild all authoritative snapshots, then drain changes
after the checkpoint.

## Race-free bootstrap

1. Register `account.change_recorded` through [`sync_accounts`](/docs/accounts/tasks/sync_accounts#account-change-feed-notifications).
2. Obtain C0 with `starting_position: "latest"`.
3. Enumerate [`list_accounts`](/docs/accounts/tasks/list_accounts), [`get_media_buys`](/docs/media-buy/task-reference/get_media_buys), [`list_creatives`](/docs/creative/task-reference/list_creatives), financials,
delivery, and other advertised reads for the account. Include all lifecycle
statuses and exhaust every page.
4. Drain changes after C0. Locally allowlist `repair.task`, construct and
validate the read request from the authenticated account and `resource`
identity, then invoke it. Never dispatch feed-supplied task arguments.
5. Persist the returned cursor and repeat after each signed notification.
6. Poll periodically so a missed webhook does not create a gap.

The webhook's optional `through_cursor` is only a target watermark. Do not
install it without reading all intervening pages.

## Shared-account example

Suppose a connected platform changes `cr_8421` while the buyer is idle. The
seller records the creative revision, makes it visible on [`list_creatives`](/docs/creative/task-reference/list_creatives),
appends the account change, and then fires `account.change_recorded`. The buyer
drains from its cursor and rereads `list_creatives`; it does not treat the
webhook payload as the creative document.

This is the same flow for a seller operator changing a campaign budget or
another authorized buyer pausing a package. Account visibility is based on
current authorization, not on which principal created the resource.

## Relationship to `webhook_activity`

`webhook_activity[]` answers whether a webhook delivery was attempted. This
feed answers which material business-state changes were recorded. A change can
exist even when a webhook is missed; one change can also have several delivery
attempts. Never use the transport log as account history.
7 changes: 6 additions & 1 deletion docs/accounts/tasks/list_accounts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ testable: false

Returns all accounts the authenticated agent can operate on this vendor agent. Use this to discover existing accounts, check status changes on pending accounts, and recover the exact account reference expected on protocol operations.

Account visibility is source-neutral: the seller MUST return every account the
authenticated caller can operate, including pre-existing accounts created or
managed through seller and connected-platform surfaces. `list_accounts` is not
limited to relationships first provisioned through [`sync_accounts`](/docs/accounts/tasks/sync_accounts).

For upstream-managed account namespaces, `list_accounts` is not optional discovery polish; it is the namespace discovery contract. The upstream platform owns the accessible account set, so buyers MUST resolve an explicit `account_id` before the first account-scoped request. If the authenticated credential can access more than one account, the seller MUST expose `list_accounts`; if it can access exactly one account, the seller SHOULD expose `list_accounts` returning that singleton so SDKs can auto-select it and still send `{ "account_id": "..." }` on required-account calls. [`sync_accounts`](/docs/accounts/tasks/sync_accounts) provisioning does not create account-id accounts in 3.0.x unless a future explicit capability declares that mode; if `sync_accounts` is exposed on these sellers today, use it only for settings updates against an account already identified by `account_id`.

`list_accounts` works across all vendor protocols — media buy agents, signals agents, governance agents, and creative agents all return accounts through this same task.
Expand Down Expand Up @@ -104,7 +109,7 @@ seller-assigned `account_id` or by the complete buyer-declared natural key.
| `governance_agents` | Governance agent endpoints registered on this account. Present when governance agents have been configured via [`sync_governance`](/docs/accounts/tasks/sync_governance). |
| `setup` | Present when `status: "pending_approval"`. Contains `url` for completing setup and `message` explaining what's needed. |
| `authorization` | Optional. The calling agent's scope grant for this account — `allowed_tasks`, `field_scopes`, `scope_name`, `read_only`. Applies to every vendor agent type (media-buy, signals, governance, creative, brand) — the Accounts Protocol surface is shared. Vendor agents that support scope introspection SHOULD populate this; media-buy sales agents claiming the `attestation_verifier` standard scope MUST populate it. Absence means the vendor agent does not advertise introspectable scope for this account; callers MUST NOT infer access from absence and fall back to error-driven discovery via the RBAC error codes. See [Caller authorization](/docs/accounts/overview#caller-authorization) for the full shape and semantics. |
| `notification_configs` | Account-level webhook subscribers registered via [`sync_accounts`](/docs/accounts/tasks/sync_accounts#account-level-webhook-subscriptions). Each entry carries `subscriber_id`, `url`, `event_types[]`, and `active`. Present when the account has any persisted subscribers. `subscriber_id` is the account-scoped logical key; re-registering the same subscriber replaces that subscriber's config. `authentication.credentials` is omitted on every entry (write-only). Use this surface to verify what's active after a sync, audit fan-out across multiple subscribers, and detect drift between buyer-side expectations and seller-side persisted state. `account.status_changed` subscribers receive status invalidation fires and repair by re-reading this `status` field. |
| `notification_configs` | Account-level webhook subscribers registered via [`sync_accounts`](/docs/accounts/tasks/sync_accounts#account-level-webhook-subscriptions). Each entry carries `subscriber_id`, `url`, `event_types[]`, and `active`. Present when the account has any persisted subscribers. `subscriber_id` is the account-scoped logical key; re-registering the same subscriber replaces that subscriber's config. `authentication.credentials` is omitted on every entry (write-only). Use this surface to verify what's active after a sync, audit fan-out across multiple subscribers, and detect drift between buyer-side expectations and seller-side persisted state. `account.status_changed` subscribers repair by rereading this account; `account.change_recorded` subscribers drain [`list_account_changes`](/docs/accounts/tasks/list_account_changes) and invoke each record's repair task. |
| `webhook_activity` | Optional recent webhook delivery attempts for this account, returned when `include_webhook_activity: true` and the seller exposes the debug log. Omitted means unsupported or not requested; `[]` means supported but no retained fires; non-empty records are most recent first. |

For buyer-declared accounts, `list_accounts` MUST return the current canonical natural-key fields needed to use the account again. A stateless buyer can therefore take `brand`, `operator`, `operator_unit`, `currency`, buyer-selected `timezone`, and `sandbox` from the response and send the same shape as `account` on a later task. `operator_unit.name` may change without changing which account the key identifies. A requested identity in `identity_change` is not a usable account reference until it becomes canonical.
Expand Down
20 changes: 19 additions & 1 deletion docs/accounts/tasks/sync_accounts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,24 @@ Indicator and assignment subscriptions are prospective: activation or reactivati

Before relying on durable account lifecycle webhooks, read `get_adcp_capabilities.account.notifications`. Sellers that declare `supported: true` accept `account.status_changed` registrations here, name `sync_accounts` as the registration task, and name [`list_accounts`](/docs/accounts/tasks/list_accounts) as the repair read. Sellers that omit the capability or declare `supported: false` MUST reject `account.status_changed` registrations instead of silently storing a subscriber that will never fire.

### Account change feed notifications

`account.change_recorded` is the generic wake-up for the optional durable
[`list_account_changes`](/docs/accounts/tasks/list_account_changes) feed. It
covers committed material changes from every origin within the seller's
advertised resource coverage, including seller surfaces, connected platforms,
seller automation, and other authorized principals. Before accepting this
event type, the seller MUST advertise
`get_adcp_capabilities.account.change_feed.supported: true`.

Each feed record produces one logical fire for every active subscriber that
requested `account.change_recorded`. The fire's `notification_id` equals the
record's `change_id`. The payload is only an invalidation: receivers drain from
their own persisted cursor and then invoke the record's repair task. Its
optional `through_cursor` is a target watermark, never a checkpoint to install
without reading intervening pages. Existing specialized notifications may
overlap this generic fire.

For these event types, "wholesale feed" means the seller's buyable wholesale product and signals feeds returned by [`get_products`](/docs/media-buy/task-reference/get_products) or [`get_signals`](/docs/signals/tasks/get_signals); it is not the buyer-provided feeds managed by [`sync_catalogs`](/docs/media-buy/task-reference/sync_catalogs).

Permitted in **both** provisioning and settings-update modes. Declarative semantics:
Expand All @@ -289,7 +307,7 @@ Each entry has:

- `subscriber_id` — buyer-supplied identifier, unique within the account; echoed on every fire so multi-subscriber accounts can route by endpoint
- `url` — HTTPS endpoint URL. Sellers MUST complete an endpoint activation challenge or equivalent proof-of-control before treating a new or changed active subscriber as active.
- `event_types[]` — types the subscriber wants. Only account-anchored types are permitted (today: `creative.status_changed`, `creative.assignment_changed`, `indicators.changed`, `creative.purged`, `account.status_changed`, `product.created`, `product.updated`, `product.priced`, `product.removed`, `signal.created`, `signal.updated`, `signal.priced`, `signal.removed`, `wholesale_feed.bulk_change`). Sellers MUST reject any media-buy-anchored type (`scheduled`, `final`, `delayed`, `adjusted`, `window_update`, `impairment`) or agent-anchored type (`capabilities.changed`) as a per-account validation failure with [`INVALID_REQUEST`](/docs/building/verification/compliance-catalog#error-code-invalid-request) or [`VALIDATION_ERROR`](/docs/building/verification/compliance-catalog#error-code-validation-error) in `accounts[].errors[]`, and `error.field` MUST point at the invalid `event_types` entry.
- `event_types[]` — types the subscriber wants. Only account-anchored types are permitted (today: `creative.status_changed`, `creative.assignment_changed`, `indicators.changed`, `creative.purged`, `account.status_changed`, `account.change_recorded`, `product.created`, `product.updated`, `product.priced`, `product.removed`, `signal.created`, `signal.updated`, `signal.priced`, `signal.removed`, `wholesale_feed.bulk_change`). Sellers MUST reject any media-buy-anchored type (`scheduled`, `final`, `delayed`, `adjusted`, `window_update`, `impairment`) or agent-anchored type (`capabilities.changed`) as a per-account validation failure with [`INVALID_REQUEST`](/docs/building/verification/compliance-catalog#error-code-invalid-request) or [`VALIDATION_ERROR`](/docs/building/verification/compliance-catalog#error-code-validation-error) in `accounts[].errors[]`, and `error.field` MUST point at the invalid `event_types` entry.
- `product_payload_view` — `canonical` for a [`list_products`](/docs/media-buy/task-reference/list_products) mirror or `legacy` for the 3.x `get_products` shape. Omission defaults to `legacy`; valid only when a product event is selected.
- `authentication` (optional) — legacy Bearer or HMAC-SHA256. Omit to use the default RFC 9421 webhook profile. When present, the same signed-registration downgrade-resistance rules as `push_notification_config.authentication` apply. Credentials are write-only — sellers omit them on reads.
- `active` (default `true`) — set `false` to pause a subscriber without removing the registration. Sellers MAY skip only the outbound proof challenge while `active: false`; they MUST still enforce HTTPS parsing, hostname normalization, and reserved-range rejection on write. Paused subscribers MUST NOT receive fires until reactivated. Reactivation MUST repeat full SSRF validation with connect pinning plus proof-of-control for any tuple without current valid proof.
Expand Down
8 changes: 8 additions & 0 deletions docs/creative/task-reference/list_creatives.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ description: "list_creatives browses and filters creatives in an AdCP library by

Browse and filter creatives in a creative library. Supports filtering by asset type, format, status, concept, tags, date range, and dynamic variables, with pagination and optional field enrichment.

For every account the caller can access, the library membership is
**source-neutral**: sellers MUST return creatives the caller can see regardless
of whether they were created through [`sync_creatives`](/docs/creative/task-reference/sync_creatives), a seller UI or API,
seller automation, another authorized principal, or a connected platform.
Filters and lifecycle defaults still apply. An implementation that exposes
only AdCP-created creatives is not a conforming account snapshot.

Implemented by any agent that hosts a creative library — creative agents (ad servers, creative management platforms) and sales agents that manage creatives.

**Response time**: ~1 second (simple database lookup)
Expand All @@ -22,6 +29,7 @@ Implemented by any agent that hosts a creative library — creative agents (ad s
- Filter by creative concept (groups of related creatives across sizes/formats)
- Find DCO creatives and inspect their dynamic content slots
- Find creatives with seller indicators such as package-scoped creative fatigue
- Reconcile source-neutral external changes through the optional account change feed

## Request parameters

Expand Down
Loading
Loading