> ## Documentation Index
> Fetch the complete documentation index at: https://docs.adcontextprotocol.org/llms.txt
> Use this file to discover all available pages before exploring further.

# list_account_changes

> list_account_changes reads the durable ordered feed of material changes to authoritative state on one shared advertiser account.

`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.

> [!NOTE]
>
> **Experimental.** The account change feed (`list_account_changes`, the `account.change_recorded` webhook, the `account.change_feed` capability block on [`get_adcp_capabilities`](/dist/docs/3.3.0-beta.0/protocol/get_adcp_capabilities), and the [`CURSOR_EXPIRED`](/dist/docs/3.3.0-beta.0/building/verification/compliance-catalog#error-code-cursor-expired) error code) is part of AdCP 3.2 as an experimental surface. [RFC #6810](https://github.com/adcontextprotocol/adcp/issues/6810) is still open, so the surface may change between 3.x releases with at least 6 weeks' notice. Release 3.3 adds the `idempotency_key` echo and requires `changed_paths` for `adcp` and `seller_operator` records, both described below. Sellers implementing any part of it MUST declare `account.change_feed` in `experimental_features`. See [experimental status](/dist/docs/3.3.0-beta.0/reference/experimental-status) for the full contract.

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

## 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 design spec](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/3.3.0-beta.0/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, authorization epoch, account, and filters. Mutually exclusive with `starting_position`. |
| `starting_position` | No | `earliest` (intentional completeness-first 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 theme={null}
{
  "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/3.3.0-beta.0/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 mechanically evaluable freshness. A connected source reporting `current` supplies `last_successful_sync_at` and `stale_after_seconds`. |

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

`resource_revision` carries the resource family's seller revision when one is
defined: `revision` for a media buy or account, and `seller_revision` for a
creative. It is the value immediately after that change, in the same JSON type
the read uses, so a buyer can tell whether a read it already holds reflects
the change; a read returning a different value reflects later changes. The seller advances that revision on every committed change from
any origin, including the changes this feed records for seller operators,
seller automation and connected platforms. Families without a seller revision
(package, creative assignment, audience, catalog, event source, property list,
product and signal as of 3.3) omit it, and the buyer repairs by rereading. A record coalesced under its media buy may carry the media buy's `revision`, because the repair read is [`get_media_buys`](/dist/docs/3.3.0-beta.0/media-buy/task-reference/get_media_buys). Records for state the revision does not cover, such as `health` and `impairments`, omit it.

### `changed_paths`

`changed_paths` lists RFC 6901 JSON Pointers to the material fields that
changed, so a buyer can decide whether a change matters before it rereads.

* When `origin.kind` is `adcp` or `seller_operator`, the seller processed the
  mutation and knows what it changed, so the record MUST carry a non-empty
  `changed_paths`. When the whole resource was created, deleted, or purged,
  use the root pointer `""`.
* When `origin.kind` is `seller_system` or `connected_platform`, `changed_paths`
  stays best-effort. A seller that observed a change through a source that does
  not report what moved omits it, and the buyer rereads the resource.
* Paths are RFC 6901 pointers into the document the `repair.task` read returns
  for the resource. Array indices are positions at the time of the change and
  can shift when elements are removed or reordered, so treat a path as a hint
  about what to look at, not as a stable address.
* The list is a cover, not an exhaustive diff. When a change touches more than
  the 64 paths a record can hold, name the nearest common ancestor pointer, or
  `""` if there is none. Do not truncate.
* Origin kinds follow where the seller learned of the change: `adcp` is an AdCP
  task call, `seller_operator` is seller staff acting through the seller's own
  surface, `seller_system` is seller automation, and `connected_platform` is a
  change the seller observed in an external system, whoever made it there. A
  seller does not relabel a change to avoid the `changed_paths` requirement.
* A `changed_paths` of `""` means reread the whole resource and is the only
  entry in that list.
* Paths never carry values, and the buyer still repairs from the authoritative
  read rather than from the paths.
* A 3.2 seller can omit `changed_paths` on these origins. Buyers tolerate that,
  treat it as unknown, and reread the resource.

### Recognizing your own changes

An `adcp` change record can echo the `idempotency_key` of the request that
produced it, so a buyer on a shared account can tell its own mutations from
another principal's.

* The seller includes `idempotency_key` only on records whose `origin.kind` is
  `adcp`, and only on records served to the principal that sent that request.
  Every other reader of the account sees the record with the field omitted.
* Apart from that field, records are the same across readers. The seller does
  not vary whether a record appears, or its `change_id`, `resource`, `action`,
  `origin.kind`, or `changed_paths`, by reader. Redaction the schema already
  allows, such as `actor` and `origin.connection_id`, stays reader-specific.
* The key is a correlation token for the sender's own use. Compare it with the
  keys the buyer sent; a match means the change came from that request. A buyer
  that sees no key on an `adcp` record does not conclude another principal made
  the change, because a seller without the echo omits it for everyone. "The
  principal that sent the request" is the principal whose authenticated scope
  the key was evaluated under, the same identity the cursor binds to, not a
  particular user or credential behind it. A seller that echoes the key keeps the
  originating principal and key for the full retention window, which outlasts
  the idempotency replay window.
* One request can produce several records, such as one per creative, so match
  the keys you sent as a set. A key can recur outside the replay window, so read
  a match against a record's `recorded_at`. A mutation that completes
  asynchronously keeps the original request's key and principal.
* A seller echoes the key only when the record covers exactly one originating
  request. A record that coalesces changes from several requests, for example
  package changes folded under a media buy, omits it. A record never coalesces
  changes across origin kinds or principals.
* A match is a dedupe hint, not a substitute for the authoritative read. The
  seller may have normalized the change, or a later change may have landed since,
  so reread unless the mutation response already carried the final state. Persist
  keys before sending requests, and keep them for as long as you want to
  recognize the record, up to the 90-day retention window.
* `account.change_recorded` notifications never carry the originating key,
  because the notification is an invalidation that every subscriber on the
  account receives, and the key belongs to one principal's view of the feed. The
  `idempotency_key` on that payload is the delivery key for one
  fire, never a request key. Read the record from the feed.

```json theme={null}
{
  "change_id": "chg_01K38H2R5D7B0V9N3C4S8E6J1P",
  "recorded_at": "2026-08-24T12:04:19Z",
  "resource": {
    "type": "media_buy",
    "account_id": "acc_luma_shared",
    "resource_id": "mb_5512"
  },
  "action": "updated",
  "origin": { "kind": "adcp" },
  "idempotency_key": "9f2c1d7e-3b5a-4c68-8e10-a7d4b2f6c901",
  "changed_paths": ["/packages/0/budget"],
  "repair": { "task": "get_media_buys" }
}
```

The same record served to any other principal on `acc_luma_shared` carries the
same `change_id`, `resource`, `action`, `origin.kind`, and `changed_paths`, minus
`idempotency_key`.

```json theme={null}
{
  "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"
      },
      "resource_revision": 12,
      "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
MUST reject a principal, account, or filter mismatch with [`INVALID_REQUEST`](/dist/docs/3.3.0-beta.0/building/verification/compliance-catalog#error-code-invalid-request) at
`cursor`; applying new filters at an old position could permanently skip
history.

The cursor also binds an authorization-scope epoch. If visible resources for
the principal expand or contract, the seller returns [`CURSOR_EXPIRED`](/dist/docs/3.3.0-beta.0/building/verification/compliance-catalog#error-code-cursor-expired) with
`details.reason: "authorization_scope_changed"`. Rebootstrap snapshots so
changes skipped under the old visibility cannot become an invisible gap.

Sellers retain changes for at least 90 days. An expired cursor returns
[`CURSOR_EXPIRED`](/dist/docs/3.3.0-beta.0/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`](/dist/docs/3.3.0-beta.0/accounts/tasks/sync_accounts#account-change-feed-notifications).
2. Obtain C0 with `starting_position: "latest"`.
3. Enumerate [`list_accounts`](/dist/docs/3.3.0-beta.0/accounts/tasks/list_accounts), [`get_media_buys`](/dist/docs/3.3.0-beta.0/media-buy/task-reference/get_media_buys), [`list_creatives`](/dist/docs/3.3.0-beta.0/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.

## Invoices

A seller that lists `invoice` in `account.change_feed.resource_types` tells the buyer about invoices through this feed. There is no separate invoice notification type; the buyer receives the signed `account.change_recorded` notification and drains the feed like any other resource.

Invoice records name the invoice and point at [`get_account_financials`](/dist/docs/3.3.0-beta.0/accounts/tasks/get_account_financials) for repair:

```json theme={null}
{
  "change_id": "chg_01K4B2T7QH8V3N5R6D9W0X1Y2Z",
  "recorded_at": "2026-09-01T09:15:00Z",
  "resource": {
    "type": "invoice",
    "account_id": "acc_luma_shared",
    "resource_id": "inv_2026_08_0042"
  },
  "action": "created",
  "origin": { "kind": "seller_system" },
  "repair": { "task": "get_account_financials" }
}
```

| Action | When |
| - | - |
| `created` | The invoice first becomes visible to the buyer, normally at issue. A `draft` invoice is not recorded until it is visible to the buyer. |
| `status_changed` | A later status transition. `reason` is the new invoice status: `issued`, `paid`, `past_due`, or `void`. |
| `updated` | An amount or period correction. |

`recorded_at` and `occurred_at` carry the time of the change; the invoice has no separate issue-time field. Changes to spend, credit, or payment status keep `type: "account_financials"` with `resource_id` equal to the account id.

**Repair completeness.** An implementation MUST NOT advertise a resource type whose current material state cannot be recovered through its named read (see the [coverage matrix](https://github.com/adcontextprotocol/adcp/blob/main/specs/account-change-feed.md#authoritative-coverage-matrix)). A seller that lists `invoice` in `resource_types` therefore MUST return, in `get_account_financials.invoices[]` (overriding any seller limit on the list), each invoice that has a change record inside the retention window (`account.change_feed.retention_days`) whenever the request's `period` overlaps that invoice's `period`. A request without `period` MUST return every such invoice whatever its `period`, so a buyer repairing from a change record omits `period`.

**Intermediaries.** An intermediary that consolidates billing is the seller of record for the invoices it issues. It records them on its own feed to its buyer and does not relay upstream sellers' invoice records.

Sellers that do not offer the experimental feed have no invoice notification; buyers keep polling `get_account_financials`.

## Change granularity

The feed contains at least one record per independently repairable identity.
Several package or assignment changes may coalesce under their media buy when
[`get_media_buys`](/dist/docs/3.3.0-beta.0/media-buy/task-reference/get_media_buys) repairs the complete changed closure; changes to independent
creatives require separate records. Records from one operation may share
`batch_id`, but retain independent `change_id` values and notifications.

For seller-mediated mutations, snapshot and records commit atomically. For an
external platform, records commit with the seller's ingestion of the observed
change. A pass-through authoritative read may lead the feed only within the
source's declared `stale_after_seconds` freshness bound.

## 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`](/dist/docs/3.3.0-beta.0/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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.