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

# sync_accounts

> sync_accounts provisions buyer-declared accounts or updates settings on existing accounts with an AdCP seller agent.

> [!NOTE]
>
> **Superseded where the seller supports `account.requests`** ([#8543](https://github.com/adcontextprotocol/adcp/issues/8543)). There, open and change accounts with [`request_account`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/request_account), read accounts and their requests with [`list_accounts`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/list_accounts), and, where the seller supports them (`account.status_changed` in `caller_event_types`, `media_buy.reporting_subscriptions`), register webhooks and reporting on the principal with [`sync_principal`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/protocol/sync_principal). `sync_accounts` stays stable and available in 3.3, and it is the path with sellers that don't support `account.requests`. Its formal deprecation follows `account.requests` leaving experimental status.
>
> An entry that carries only `notification_configs` and/or `reporting_delivery_configs` is a **subscription write**. It MUST name an account the caller already holds (otherwise [`ACCOUNT_NOT_FOUND`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-account-not-found)), and the seller MUST NOT provision, run account review, or change account attributes for it. A subscription write does not advance the account's `revision`.

Sync advertiser accounts with a seller for one or more advertiser/operator relationships, or update settings on existing accounts when the seller exposes that mode. A provisioning entry names the account's parties, a brand and an operator. It also sets the account's attributes, such as the countries it serves, an operator-owned unit, a fixed currency, a timezone, and sandbox status. The seller returns the `account_id` that identifies the account from then on.

`sync_accounts` is used across all seller protocols: media buy agents, signals agents, governance agents, and creative agents. In provisioning mode it declares the buyer's intent — the seller provisions or links accounts internally. Use provisioning mode for buyer-declared accounts (`require_operator_auth: false`). Sellers MUST return the seller-assigned `account_id` on every non-failed provisioning result once the account is resolved, and accept it on every account-scoped task. A seller never issues an `account_id` it might later change: a `pending_approval` row that may still resolve to a different account carries an experimental `request_id` instead. Buyers send `{ "account_id": "..." }` on subsequent requests. Sellers keep accepting the natural-key `AccountRef` throughout 3.x. Using it as the reference after the account opens is deprecated as of 3.3 and planned for removal in 4.0; see [Account identity](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/overview.md#account-identity). For account-id namespaces, discover seller-assigned account IDs via [`list_accounts`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/list_accounts) or out-of-band onboarding; `sync_accounts` provisioning for account-id namespaces is out of scope unless a future explicit capability declares that mode. If such a seller exposes `sync_accounts` today, use only settings-update mode keyed by `account_id`.

For a complete `pending_approval` flow — capability discovery, human setup, durable status notification, authoritative repair, and first spend — see [Provision a seller-mediated account](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/provisioning-walkthrough.md).

When the seller advertises `account.requests` (experimental in 3.3), prefer [`request_account`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/request_account) for provisioning. Its requests can resolve to an existing account without changing an `account_id`. Provisioning rows here are then a projection of the same request; see [Relation to `sync_accounts`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/request_account#relation-to-sync_accounts).

**Response Time**: \~1s. Account provisioning is synchronous; credit and legal review may require human action (indicated by `status: "pending_approval"` with a `setup.url`).

**Request Schema**: [`/schemas/3.3.0-beta.1/account/sync-accounts-request.json`](https://adcontextprotocol.org/schemas/3.3.0-beta.1/account/sync-accounts-request.json)
**Response Schema**: [`/schemas/3.3.0-beta.1/account/sync-accounts-response.json`](https://adcontextprotocol.org/schemas/3.3.0-beta.1/account/sync-accounts-response.json)

## Quick start

Sync a single advertiser account and check the resulting status:

```javascript JavaScript theme={null}
import { testAgent } from "@adcp/sdk/testing";
import { SyncAccountsResponseSchema } from "@adcp/sdk";

const result = await testAgent.syncAccounts({
  accounts: [
    {
      brand: { domain: "acme-corp.com" },
      operator: "acme-corp.com",
      billing: "operator",
    },
  ],
});

if (!result.success) {
  throw new Error(`Request failed: ${result.error}`);
}

const validated = SyncAccountsResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

for (const account of validated.accounts) {
  console.log(`${account.brand.domain}: ${account.status}`);
  if (account.status === "pending_approval" && account.setup?.url) {
    console.log(`  Complete setup at: ${account.setup.url}`);
  }
}
```

```python Python theme={null}
import asyncio
from adcp.testing import test_agent

async def main():
    result = await test_agent.simple.sync_accounts(
        accounts=[
            {
                "brand": {"domain": "acme-corp.com"},
                "operator": "acme-corp.com",
                "billing": "operator",
            },
        ],
    )

    if hasattr(result, 'errors') and result.errors:
        raise Exception(f"Operation failed: {result.errors}")

    for account in result.accounts:
        print(f"{account.brand['domain']}: {account.status}")
        if account.status == 'pending_approval' and hasattr(account, 'setup') and account.setup:
            print(f"  Complete setup at: {account.setup.url}")

asyncio.run(main())
```

## Request parameters

| Parameter | Type | Required | Description |
| - | - | - | - |
| `accounts` | array | Yes | Array of account entries to sync (see below). |
| `delete_missing` | boolean | No | When true, the seller deactivates accounts this agent previously synced that this request does not include. An account counts as included only when an entry names it by `account` or the seller returns its `account_id` for an entry. A provisioning entry can open an account, so list each account you keep as an `{ "account": { "account_id": "..." } }` entry. Scoped to the authenticated agent. Default: `false`. |
| `dry_run` | boolean | No | When true, preview what would change without applying. Default: `false`. |
| `push_notification_config` | object | No | Webhook for async notifications when account status changes (e.g., `pending_approval` transitions to `active`). |

**Account entry fields:**

| Field | Type | Required | Description |
| - | - | - | - |
| `account` | object | Settings update | Existing account to update, selected by `account_id`, or by a natural key that matches exactly one account. Its presence selects settings-update mode and prevents provisioning side effects. |
| `revision` | integer | With `operator_identity` | Expected current account revision in settings-update mode. It is required with the deprecated `operator_identity` and optional for every other settings update. Sellers reject stale revisions atomically with [`CONFLICT`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-conflict). Obtain it from [`list_accounts`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/list_accounts) or the latest `sync_accounts` result. |
| `operator_identity` | object | No | **Deprecated in 3.3.** Complete desired operator identity in settings-update mode. Requires the deprecated `account.identity_updates.supported: true`. Set `operator_unit` directly instead; see [Updating account attributes](#updating-account-attributes). |
| `destination_billing_entity` | object | Operator-billed domain handoff | **Deprecated in 3.3** with `operator_identity`. Complete staged billing identity for the destination operator. Required when changing `operator` on an account with `billing: "operator"`; otherwise absent. It is write-only while approval is pending and does not replace the canonical `billing_entity` until the handoff applies. |
| `brand` | object | Provisioning | Brand reference identifying the advertiser. `domain` and optional `brand_id` identify the brand. `countries[]`, a set of ISO 3166-1 alpha-2 codes, is an account attribute; it does not target delivery. In settings-update mode, `brand` updates `countries[]` and its `domain` and `brand_id` must equal the account's. The existing 3.x task still accepts broader BrandRef fields for compatibility, but per-call fields such as `industries`, `data_subject_contestation`, and `brand_kit_override` never affect lookup or account creation. New producers should send only BrandKey fields. |
| `operator` | string | Provisioning | Domain of the entity operating on the brand's behalf (e.g. `pinnacle-media.com`). When the brand operates directly, set to the brand's domain. Verified against the brand's `authorized_operators` in brand.json. |
| `operator_unit` | object | No | Operator-owned business unit, agency seat, or buying-platform account. An account attribute: `id` is the operator-assigned identifier and optional `name` is display metadata. This is not the seller's `account_id`. |
| `currency` | string | No | ISO 4217 transaction currency when the seller's advertiser object is currency-bound. Check `account.supported_account_currency_modes`: include `currency` for `fixed`; omit it for `per_media_buy`. If an older 3.x seller omits that capability, its currency model is not discoverable and must be established out of band. An account attribute set when the account opens: every media buy on the account must use it. See [Updating account attributes](#updating-account-attributes). |
| `timezone` | string | Conditional | Operational timezone, an account attribute set when the account opens. Check `get_adcp_capabilities.account.timezone`: include one of `supported_timezones` when mode is `account_fixed` and selection is `buyer_selected`; omit it for `seller_fixed` or `seller_assigned`. The seller echoes the effective timezone on `sync_accounts` and `list_accounts`. |
| `billing` | string | Provisioning | Who the seller invoices for this buyer–storefront account relationship: `operator`, `agent`, or `advertiser`. An account attribute: a settings-update entry changes it on the same account. Check [`get_adcp_capabilities`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/protocol/get_adcp_capabilities) for `supported_billing`. The seller must accept the requested invoiced party exactly or reject the request. This field does not choose a payment rail, clearing intermediary, or per-media-buy settlement route; AdCP payment and settlement remain out of protocol. Sellers MAY additionally reject a seller-wide supported value when the calling buyer agent's commercial relationship does not permit it. See [`BILLING_NOT_SUPPORTED`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-billing-not-supported), [`BILLING_NOT_PERMITTED_FOR_AGENT`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-billing-not-permitted-for-agent), [Buyer-agent identity](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/by-layer/L2/accounts-and-agents.md#buyer-agent-identity), and [Billing and Account Setup](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/by-layer/L3/error-handling.md#billing-and-account-setup). |
| `billing_entity` | object | No | Structured business entity details for the account's current invoiced party. Contains `legal_name` (required), plus optional `vat_id`, `tax_id`, `registration_number`, `address`, `contacts`, and `bank`. Bank details are write-only — included in requests but never echoed in responses. Do not use this field to stage a destination operator during a handoff; use `destination_billing_entity`. See [billing entity and invoice recipient](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/by-layer/L2/accounts-and-agents.md#billing-entity-and-invoice-recipient). |
| `payment_terms` | string | No | Payment terms for this account: `net_15`, `net_30`, `net_45`, `net_60`, `net_90`, or `prepay`. The seller must either accept these terms or reject the account — terms are never silently remapped. When omitted, the seller applies its default terms. |
| `sandbox` | boolean | No | When true, set up a sandbox account with no real platform calls or billing. An account attribute set when the account opens; it never changes. Only applicable to buyer-declared accounts (`require_operator_auth: false`). For account-id namespaces, sandbox accounts are pre-existing test accounts discovered via [`list_accounts`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/list_accounts) or supplied out-of-band. |
| `notification_configs` | array | No | Account-level webhook subscribers for events that outlive any single media buy: creative lifecycle notifications, account status changes, and wholesale feed change webhooks. Omit to leave existing subscribers unchanged; send `[]` to remove all subscribers; send a full array to replace. Entries are keyed by account-scoped `subscriber_id`; an existing `subscriber_id` is upserted, and persisted IDs absent from the sent array are removed. |
| `reporting_delivery_configs` | array | No | Experimental caller/account-owned durable reporting desired state. Each entry selects one atomic capability `offering_id` and its exact `report_definition_id`, chooses `delivery_only` or `consumer_receipt` reconciliation, and creates an immutable semantic `(delivery_config_id, delivery_config_version)` generation; only lifecycle intent may change in place. Billing uses `consumer_receipt`. Omit to preserve the caller's set; send `[]` to deactivate it. Destination coordinates are secret-free, and a caller-scoped immutable `destination_ref` may be reused across separately authorized accounts. A `delivery_config_id` equal to one of the caller's [reporting subscription](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/protocol/sync_principal.md#reporting-subscriptions) `subscription_id` values is rejected, and an active entry takes precedence over the caller's own subscriptions for this account and its `feed_purpose`, never another caller's. See [`get_reporting_status`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/media-buy/task-reference/get_reporting_status). |

**Account identity**: The seller-assigned `account_id` identifies the account. `brand.domain`, optional `brand.brand_id`, and `operator` name the account's parties. `brand.countries`, `operator_unit`, `currency`, `timezone`, and `sandbox` are account attributes: an entry sets them, and they never change which account the `account_id` names. Country order has no meaning, and per-call BrandRef fields never affect lookup or account creation.

An entry without `account` opens an account. A retry with the same `idempotency_key` returns the original result instead of opening a second account.

Sending a provisioning entry again is safe. When the entry matches exactly one account in the seller's [account scope](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/by-layer/L2/accounts-and-agents.md#account-scope), the seller SHOULD return that account's `account_id` instead of opening another, so a buyer that lost its idempotency state doesn't open a duplicate. The entry matches as a natural-key reference does: on `brand` and `operator`, with each attribute it supplies as a filter; see [Natural key semantics](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/by-layer/L2/accounts-and-agents.md#natural-key-semantics). This is the match the [`request_account` deduplication guidance](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/request_account#deduplication) recommends, and billing terms never filter. To open a second account for the same brand, such as one per market, send an entry whose attributes differ, such as its `countries`.

A provisioning entry never changes an existing account. When the seller returns an existing account, the result row echoes the account's own attributes, and the exact-or-reject rules for `billing` and `payment_terms` still apply. To change an attribute, use settings-update mode.

For example, the following fields describe Nova's account for Germany and the Netherlands, operated through Pinnacle's EMEA seat and fixed to EUR:

```json theme={null}
{
  "brand": {
    "domain": "nova-athletics.example",
    "countries": ["DE", "NL"]
  },
  "operator": "pinnacle-media.example",
  "operator_unit": {
    "id": "seat_emea_01",
    "name": "EMEA"
  },
  "currency": "EUR"
}
```

Don't put an operator's buying-platform account ID in `account_id`. That value belongs in `operator_unit.id`; `account_id` is the seller's identifier.

### Updating account attributes

To change an attribute, such as `brand.countries` or `billing`, send a settings-update entry that names the account by `account_id`. The following entry changes the countries:

```json theme={null}
{
  "idempotency_key": "attribute-update-018f24c0-65d1-7c3a",
  "accounts": [
    {
      "account": { "account_id": "acc_nova_pinnacle" },
      "revision": 7,
      "brand": {
        "domain": "nova-athletics.example",
        "countries": ["BE", "DE", "NL"]
      }
    }
  ]
}
```

The seller applies the change to the same account, which keeps its `account_id`, media buys, and reporting history. The seller validates a changed attribute as it does when the account opens, and rejects the entry with the same error. Authorization, such as the brand-operator check, applies to the new values. The entry's `brand.domain` and `brand.brand_id` must equal the account's: a different brand is a different account, and the seller rejects the entry with [`INVALID_REQUEST`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-invalid-request). A seller that does not support changing an attribute rejects the entry with [`UNSUPPORTED_FEATURE`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-unsupported-feature) and leaves the account unchanged.

`currency`, `timezone`, and `sandbox` are set when the account opens. A seller MAY refuse a `currency` or `timezone` change with `UNSUPPORTED_FEATURE`, and changing one normally means opening a new account. `sandbox` never changes: the seller refuses a different `sandbox` value with `UNSUPPORTED_FEATURE`.

A seller can apply a `countries` change, refuse it, or review it first. To review it, the seller reports `action: "unchanged"` and `status: "pending_approval"` on the entry's result row, and `setup` can explain the review. The account keeps its current countries until the seller approves, and `list_accounts` reports the account's own status. For example:

```json theme={null}
{
  "accounts": [
    {
      "account": { "account_id": "acc_nova_pinnacle" },
      "action": "unchanged",
      "status": "pending_approval",
      "setup": {
        "message": "Adding BE requires a market review."
      }
    }
  ]
}
```

A settings-update result row echoes `account` and does not carry `brand`. To confirm a `countries` change, or to learn the outcome of a review, read the account again through [`list_accounts`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/list_accounts).

`revision` is optional and guards the write. When it is stale, the seller returns [`CONFLICT`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-conflict) without side effects. Every persisted account change increments `revision`; reads, dry runs, validation failures, and exact idempotency replays do not.

An attribute update never changes the account's identity, so no former reference exists to retire: the `account_id` still names the account.

> [!NOTE]
>
> **Deprecated in 3.3: `operator_identity` reconciliation.** The `operator_identity` and `destination_billing_entity` request fields are deprecated. So are the `identity_change` and `identity_change_preview` response fields and the `account.identity_updates` capability. The [`ACCOUNT_MOVED`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-account-moved) and [`ACCOUNT_IDENTITY_CONFLICT`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-account-identity-conflict) error codes are deprecated too. All of them are planned for removal in 4.0. Sellers that implement them keep them working through 3.x under the [3.2 `sync_accounts` rules](https://docs.adcontextprotocol.org/dist/docs/3.2.6/accounts/tasks/sync_accounts#reconciling-operator-identity). To change `operator_unit`, set it as an attribute. A different operator domain is a different party: that operator opens its own account.

## Response

**Success response:**

Returns an `accounts` array with per-account results. Individual accounts may be pending, rejected, or failed even when the operation succeeds.

**Error response:**

* `errors` -- Array of operation-level errors (auth failure, service unavailable). No `accounts` array is present.

**Note:** Responses use discriminated unions -- you get either `accounts` OR `errors`, never both.

**Per-account fields:**

| Field | Description |
| - | - |
| `account` | Present only on settings-update-mode results, echoed from the request's `account` field. Anchors this row's identity via `account_id` or natural key in place of `brand`/`operator` — including on `action: "failed"` results, where the account may not resolve to a brand/operator tuple at all. See [Two modes](#two-modes-provisioning-vs-settings-update). |
| `account_id` | Seller-assigned account identifier and the account's identity. A seller never issues an `account_id` it might later change. Sellers MUST return it on every non-failed provisioning result, except a `pending_approval` row that may still resolve to a different account, which carries `request_id` and no `account_id`. Sellers accept it as an `AccountRef` on every account-scoped task, and it never changes for the life of the account. |
| `request_id` | Experimental (`account.requests`). The seller's account request behind a `pending_approval` row that has no `account_id` yet. Read it with [`list_accounts`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/list_accounts) (`request_ids`). Never an `AccountRef`. |
| `resolution` | Experimental (`account.requests`). On a resolved row: `matched` (an existing account was linked; `action: unchanged`) or `created`. |
| `brand` | Current canonical brand. Object with `domain`, optional `brand_id`, and optional `countries[]`. Present on provisioning-mode results. |
| `operator` | Current operator domain. While a deprecated identity change is pending or rejected, this remains the current value. |
| `operator_unit` | Current operator unit, an account attribute. `name` is display metadata. |
| `revision` | Current optimistic-concurrency revision. Pass it on the next settings update. |
| `identity_change` | **Deprecated in 3.3.** Pending or rejected desired operator identity. Current values remain in `operator` and `operator_unit`. |
| `identity_change_preview` | **Deprecated in 3.3.** Dry-run-only disposition and resource-impact preview. Current values and revision remain unchanged. |
| `currency` | Fixed account currency, echoed when supplied. |
| `name` | Seller's display name for the account. |
| `action` | What happened: `created`, `updated`, `unchanged`, or `failed`. |
| `status` | Current state of the account (see [Account status](#account-status)). On a settings-update row, `pending_approval` means the seller is reviewing the entry's `countries` change. Required except when `action: "failed"`, where the seller may not know a lifecycle state to report. |
| `billing` | Billing model applied. Matches the requested value. |
| `billing_entity` | Current canonical business entity for the invoiced party. During a pending operator-domain handoff, this remains the current entity; the staged `destination_billing_entity` is write-only. Sellers may add verified fields, but bank details are always omitted. |
| `account_scope` | How the seller scoped this account: `operator` (shared across brands for this operator), `brand` (shared across operators for this brand), `operator_brand` (dedicated to this operator+brand pair), or `agent` (agent-scoped account shared across declared brand/operator pairs). See [account scope](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/by-layer/L2/accounts-and-agents.md#account-scope). |
| `setup` | Present when `status: "pending_approval"`. Contains `url` for completing credit or legal setup, `message` explaining what's needed, and optional `expires_at`. Experimental `waiting_on` (`seller` or `buyer`) says whose turn it is, and `kind` says what is needed (`review`, `sign_up`, `accept_terms`, `credit_application`, `add_payment`, `grant`, `connect_credentials`). |
| `rate_card` | Seller-assigned rate card identifier (when applicable). |
| `payment_terms` | Payment terms agreed for this account: `net_15`, `net_30`, `net_45`, `net_60`, `net_90`, or `prepay`. When the account is active, these are the binding terms for all invoices. |
| `credit_limit` | Maximum outstanding balance as `{amount, currency}`. |
| `errors` | Per-account errors. Required and non-empty when `action: "failed"`; absent otherwise. |
| `warnings` | Non-fatal notices. |
| `sandbox` | Whether this is a sandbox account, echoed from the request. Only present for buyer-declared accounts. |
| `notification_configs` | Optional. Current persisted account-level webhook subscribers after applying the request. Present on `created`, `updated`, and `unchanged` results when the request included `notification_configs` or the account already has persisted subscribers. Each entry carries `subscriber_id`, `url`, `event_types[]`, and `active`; `authentication.credentials` is omitted (write-only). |
| `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. Present on `created`, `updated`, and `unchanged` results; omitted on `failed` results. 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; callers MUST NOT infer access from absence. See [Caller authorization](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/overview.md#caller-authorization) for the full shape. |

### Account status

| Status | Meaning | Next step |
| - | - | - |
| `active` | Ready to use | Use [account reference](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/by-layer/L2/accounts-and-agents.md#account-references) in protocol operations |
| `pending_approval` | Seller reviewing the account or a `countries` change on a settings-update row, or waiting on a buyer-side human | `setup.waiting_on`, when present, says which. Human may need to visit `setup.url` to complete credit or legal process. Poll [`list_accounts`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/list_accounts) for updates, or [`list_accounts`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/list_accounts) with `request_ids` when the row carries `request_id`. |
| `rejected` | Seller declined the request | Review rejection reason in `warnings`, adjust and retry, or contact seller |
| `payment_required` | Credit limit reached or funds depleted | Add funds or increase credit limit. Route spend to other accounts. |
| `suspended` | Was active, now paused | Contact seller to resolve |
| `closed` | Was active, now terminated | -- |

### Async notifications

When `push_notification_config` is provided and the seller returns `pending_approval`, the seller sends a webhook notification when the account status changes (e.g., approved → `active`, declined → `rejected`).

For provisioning requests, the notification payload includes `brand`, `operator`, and the seller-assigned `account_id`, so the buyer can correlate it to the original sync request. The buyer uses that `account_id` on subsequent calls.

```json theme={null}
{
  "brand": { "domain": "nova-brands.com", "brand_id": "glow" },
  "operator": "pinnacle-media.com",
  "status": "active",
  "account_id": "acc_glow_001"
}
```

If the buyer did not provide `push_notification_config`, poll [`list_accounts`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/list_accounts) to check for status changes.

## Two modes: provisioning vs. settings-update

Each per-account entry uses one of two key shapes, never both:

* **Provisioning mode** — flat `brand` + `operator` + `billing` at the entry root. The seller opens an account and MUST return its `account_id` once the account is resolved. When the entry matches exactly one account in its account scope, the seller SHOULD return that `account_id` instead; see [Request parameters](#request-parameters). Used for buyer-declared accounts (`require_operator_auth: false`). This is the shape AdCP 3.0 shipped with. The natural-key `AccountRef` remains valid for subsequent calls throughout 3.x.
* **Settings-update mode** — `account` (an [AccountRef](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/by-layer/L2/accounts-and-agents.md#account-references)) at the entry root, with `operator` absent. The seller updates the account's settable state and attributes, `brand.countries` and `billing` included — no provisioning side effects. A seller that opens buyer-declared accounts through `sync_accounts` accepts settings-update entries keyed by `account_id`, and declines an attribute change it cannot make with [`UNSUPPORTED_FEATURE`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-unsupported-feature). For account-id namespaces, the mode applies only when the seller exposes settings updates through this task. Upstream-managed accounts are discovered via [`list_accounts`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/list_accounts), while seller-defined account IDs may be supplied out-of-band.

Schema enforces the exclusivity via `oneOf` — sending both shapes on the same entry is a validation error. An account-id-namespace seller that doesn't expose settings updates rejects `account`-keyed entries with [`UNSUPPORTED_PROVISIONING`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-unsupported-provisioning). Sellers that don't provision through `sync_accounts`, including account-id namespaces, reject natural-key provisioning entries with the same code.

The response result row mirrors the request's discriminator, and the two identity shapes are mutually exclusive: a settings-update-mode result echoes `account` instead of `brand`/`operator`, since an account-id-namespace seller frequently has no buyer-declared brand/operator tuple to return at all. This also makes `action: "failed"` settings-update results representable — for example, an `account_id` the seller cannot reach has no brand/operator tuple to resolve, and the row's identity is carried entirely by the echoed `account` instead. `status` is required on every non-failed result, but optional on `failed` results: some failures still know the account's lifecycle state (e.g. a rejected identity change on an otherwise-reachable account), while others — like this one — do not, and sellers MUST NOT invent a value; `errors` is required instead:

```json theme={null}
{
  "account": { "account_id": "acct-social-unreachable" },
  "action": "failed",
  "errors": [
    { "code": "ACCOUNT_NOT_FOUND", "message": "Account does not exist or is not accessible to the authenticated agent." }
  ]
}
```

## Account-level webhook subscriptions

`notification_configs[]` carries account-level webhook subscribers for creative lifecycle and assignment changes, `indicators.changed`, account status, wholesale feed changes, and future account-anchored events.

Indicator and assignment subscriptions are prospective: activation or reactivation does not replay conditions that were already current. After activation, buyers establish a complete baseline through [`get_media_buys`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/media-buy/task-reference/get_media_buys) before relying on later invalidations: enumerate known media-buy IDs or request every media-buy status and follow pagination to exhaustion, without an indicator filter. The optional bounded [`list_creatives`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/creative/task-reference/list_creatives) projection is not sufficient to prove complete state.

`account.status_changed` is the durable account lifecycle event. Use it for status transitions after the initial `sync_accounts` result, such as `pending_approval → active`, `pending_approval → rejected`, `active → payment_required`, `active → suspended`, recovery back to `active`, or terminal `closed`. It is not a replacement for the one-shot `push_notification_config` on this task: that channel can report the async result of the original provisioning operation, while `notification_configs[]` subscribers outlive the task.

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`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/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`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/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` and list the
[experimental](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/reference/experimental-status.md) feature id
`account.change_feed` in `experimental_features`.

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`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/media-buy/task-reference/get_products) or [`get_signals`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/signals/tasks/get_signals); it is not the buyer-provided feeds managed by [`sync_catalogs`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/media-buy/task-reference/sync_catalogs).

Permitted in **both** provisioning and settings-update modes. Declarative semantics:

* Omit `notification_configs` to leave the account's existing subscribers unchanged.
* Send `notification_configs: []` to remove every subscriber on that account.
* Send a non-empty array to replace the account's current set with the submitted set.

Within one account, `subscriber_id` is the stable logical key. Re-sending an existing `(account_id, subscriber_id)` with a different `url`, `event_types`, `authentication`, or `active` value replaces that subscriber's active config rather than creating a duplicate. The seller MUST NOT merge the submitted array with persisted state: persisted subscribers whose `subscriber_id` does not appear in the sent array are removed. Paused entries (`active: false`) are subject to the same replacement semantics; to preserve a paused subscription, re-include it with `active: false` in the sent array. Duplicate `subscriber_id` values within the same submitted array are invalid. The replacement is account-scoped; the same `subscriber_id` MAY be reused on a different account.

If any entry in the submitted replacement set fails validation or activation proof, the seller rejects that account entry with `action: "failed"` and leaves the account's previous `notification_configs[]` set unchanged. Sellers MUST NOT partially apply a replacement set and silently drop only the failed subscriber.

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`, `account.change_recorded`, `product.created`, `product.updated`, `product.priced`, `product.removed`, `signal.created`, `signal.updated`, `signal.priced`, `signal.removed`, `wholesale_feed.bulk_change`, and the experimental `collection_card.updated`, `collection_card.removed` and `collection_catalog.bulk_change`, which a buyer registers only with a seller that lists them in `wholesale_feed_webhooks.event_types`; see [`list_collections`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/media-buy/task-reference/list_collections#collection-card-webhooks)). 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`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-invalid-request) or [`VALIDATION_ERROR`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/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`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/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. A seller with `legacy_hmac_fallback: false` rejects an entry that carries it with [`UNSUPPORTED_FEATURE`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-unsupported-feature) in `accounts[].errors[]`. 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.

### Endpoint proof of control

Before persisting or echoing an entry as `active: true`, the seller MUST validate the URL, apply the SSRF rules in [Webhook URL validation](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/by-layer/L1/security.md#webhook-url-validation-ssrf), and prove that the receiver controls the endpoint.

Proof is required when there is no current valid proof for the tuple `(account_id, subscriber_id, normalized url, authentication mode/credential binding, normalized event_types)`. Changing the subscriber ID, normalized URL, authentication mode/credential binding, or `event_types[]` requires fresh proof before the new set can become active. The challenge POST itself MUST be signed with the seller's RFC 9421 webhook profile key even when the candidate config selects legacy delivery auth. New signers use `adcp_use: "request-signing"`; deprecated `webhook-signing` keys remain accepted during the compatibility window. The receiver MUST verify the RFC 9421 signature and MUST reject the challenge unless `account_id`, `subscriber_id`, `seller_agent_url`, `delivery_auth`, and `event_types` match the pending registration. The signature's covered `@target-uri` MUST equal the exact normalized candidate URL, and its `expires` parameter binds the challenge expiry.

The standard challenge is an HTTPS POST to the candidate `url` with a JSON body containing `type`, `challenge`, `account_id`, `subscriber_id`, `seller_agent_url`, `delivery_auth`, and `event_types`. The canonical schemas are [`webhook-challenge.json`](https://adcontextprotocol.org/schemas/3.3.0-beta.1/core/webhook-challenge.json) and [`webhook-challenge-response.json`](https://adcontextprotocol.org/schemas/3.3.0-beta.1/core/webhook-challenge-response.json).

```json theme={null}
{
  "type": "webhook.challenge",
  "challenge": "example-challenge-token-000000000000",
  "account_id": "acct_123",
  "subscriber_id": "buyer-primary",
  "seller_agent_url": "https://seller.example/adcp",
  "delivery_auth": { "mode": "rfc9421" },
  "event_types": ["creative.status_changed"]
}
```

The receiver proves control by returning HTTP `2xx` with a JSON body containing exactly one echo field:

```json theme={null}
{ "challenge": "example-challenge-token-000000000000" }
```

Sellers MUST also accept the backward-compatible alias:

```json theme={null}
{ "token": "example-challenge-token-000000000000" }
```

The challenge value MUST be cryptographically random, single-use, and scoped to the registration tuple. The RFC 9421 `expires` parameter is the challenge expiry; receivers MUST reject expired challenges, and sellers MUST reject echoes received after that instant. A failed, non-`2xx`, malformed, mismatched, expired, replayed, or timed-out challenge means proof failed. Sellers SHOULD use the same outbound fetch caps as SSRF validation (10 second connect and 10 second read) and SHOULD make at most one initial challenge POST on the `sync_accounts` critical path. A seller MAY retry one transient network failure before returning if it uses the same challenge value and still completes within its request budget; otherwise the buyer retries by re-sending `sync_accounts`.

On proof failure, the seller returns a per-account failure with `action: "failed"`, `errors[].code: "VALIDATION_ERROR"` (or `INVALID_REQUEST` for malformed URLs), and `error.field` pointing at `accounts[i].notification_configs[j].url`. The previous persisted subscriber set remains unchanged. `dry_run: true` MUST NOT send network challenges; an `active: true` echo in a dry-run result previews the requested post-proof state and does not assert that proof already exists.

### Account status payload

When an account's lifecycle status changes after the initial `sync_accounts` response, sellers fire `account.status_changed` to active subscribers whose `event_types[]` includes that value.

```json theme={null}
{
  "idempotency_key": "whk_01K18GM0Z7J3Q6WBH7DYK2R4VM",
  "notification_id": "acctchg_acc_glow_20260719T100712Z",
  "notification_type": "account.status_changed",
  "fired_at": "2026-07-19T10:07:15Z",
  "subscriber_id": "account-lifecycle",
  "account_id": "acc_glow_pending",
  "previous_status": "pending_approval",
  "status": "payment_required",
  "observed_at": "2026-07-19T10:07:12Z",
  "reason_code": "setup_required",
  "setup": {
    "message": "Complete advertiser billing setup.",
    "expires_at": "2026-07-30T00:00:00Z"
  }
}
```

The payload is an invalidation signal. It intentionally omits the full account document and `setup.url`; setup URLs are often single-use or sensitive and should not be fanned out to every subscriber. Receivers SHOULD re-run [`list_accounts`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/list_accounts) for `account_id` and reconcile from the fresh account snapshot.

`notification_id` is stable per `(account_id, previous_status, status, observed_at)`. Retries and re-emissions of the same transition reuse it under a new `idempotency_key`; a later transition cycle receives a new `notification_id`.

Example — register a buyer-side endpoint plus an audit bus on an account-id namespace account:

```json theme={null}
{
  "idempotency_key": "f2c4b7d9-6789-49bc-defa-2345678901bc",
  "accounts": [
    {
      "account": { "account_id": "acc_acme_pinnacle" },
      "notification_configs": [
        {
          "subscriber_id": "buyer-primary",
          "url": "https://buyer.example/webhooks/adcp/creative",
          "event_types": ["creative.status_changed", "creative.purged"],
          "active": true
        },
        {
          "subscriber_id": "audit-bus",
          "url": "https://audit.buyer.example/adcp/ingest",
          "event_types": ["creative.status_changed", "creative.purged"],
          "active": true
        }
      ]
    }
  ]
}
```

Example — register a wholesale feed mirror subscriber for wholesale product and signal changes:

```json theme={null}
{
  "idempotency_key": "a8af8cf1-89bd-41f3-b27d-7ee7e9f8d2e4",
  "accounts": [
    {
      "account": { "account_id": "acc_acme_pinnacle" },
      "notification_configs": [
        {
          "subscriber_id": "wholesale-feed-sync",
          "url": "https://buyer.example/webhooks/adcp/wholesale-feed",
          "event_types": [
            "product.created",
            "product.updated",
            "product.priced",
            "product.removed",
            "signal.created",
            "signal.updated",
            "signal.priced",
            "signal.removed",
            "wholesale_feed.bulk_change"
          ],
          "active": true
        }
      ]
    }
  ]
}
```

Example — register an account lifecycle subscriber:

```json theme={null}
{
  "idempotency_key": "ee1343bb-7449-4d33-a2c1-c9cc0c75d0f4",
  "accounts": [
    {
      "account": { "account_id": "acc_acme_pinnacle" },
      "notification_configs": [
        {
          "subscriber_id": "account-lifecycle",
          "url": "https://buyer.example/webhooks/adcp/accounts",
          "event_types": ["account.status_changed"],
          "active": true
        }
      ]
    }
  ]
}
```

Governance agents registered via [`sync_governance`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/sync_governance) are **not** implicitly subscribed to these webhooks. If your governance agent should also receive creative-lifecycle fires, register its URL as a separate `notification_configs[]` entry — explicit, auditable, with its own `event_types[]` filter.

Verify applied state via [`list_accounts`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/list_accounts) — the response carries the current persisted `notification_configs[]` per account with credentials redacted. `sync_accounts` also echoes the current sanitized set on `created`, `updated`, and `unchanged` results when the request included `notification_configs` or any persisted subscribers already exist.

Wholesale feed notifications are registered here, not through a separate subscription task. The webhook body is [`wholesale-feed-webhook.json`](https://adcontextprotocol.org/schemas/3.3.0-beta.1/core/wholesale-feed-webhook.json): it carries a canonical product replacement for `list_products` subscribers (or the legacy Product view for `get_products` subscribers), signal changes, or a bulk-change summary plus the post-change `wholesale_feed_version`. Sellers MUST apply the same per-subscriber authorization and scope predicate used by the corresponding wholesale read before emitting each webhook. For a detected gap, repair with the mirror's last applied version; for uncertain or bulk state, omit the conditional token and replace from `list_products` / `get_signals`. See [wholesale\_feed\_webhooks](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/protocol/get_adcp_capabilities.md#wholesale_feed_webhooks) for capability declaration and event semantics.

## Common scenarios

### Agency syncing multiple brands

```javascript JavaScript theme={null}
import { testAgent } from "@adcp/sdk/testing";
import { SyncAccountsResponseSchema } from "@adcp/sdk";

const result = await testAgent.syncAccounts({
  accounts: [
    {
      brand: { domain: "nova-brands.com", brand_id: "spark" },
      operator: "pinnacle-media.com",
      billing: "operator",
    },
    {
      brand: { domain: "nova-brands.com", brand_id: "glow" },
      operator: "pinnacle-media.com",
      billing: "operator",
    },
  ],
});

if (!result.success) {
  throw new Error(`Request failed: ${result.error}`);
}

const validated = SyncAccountsResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

for (const account of validated.accounts) {
  if (account.status === "active") {
    console.log(`Ready: ${account.brand.domain}/${account.brand.brand_id} → ${account.status}`);
  } else if (account.status === "pending_approval") {
    console.log(`Setup required for ${account.brand.brand_id}: ${account.setup?.url}`);
    // Poll list_accounts until status becomes active
  }
}
```

```python Python theme={null}
import asyncio
from adcp.testing import test_agent

async def main():
    result = await test_agent.simple.sync_accounts(
        accounts=[
            {
                "brand": {"domain": "nova-brands.com", "brand_id": "spark"},
                "operator": "pinnacle-media.com",
                "billing": "operator",
            },
            {
                "brand": {"domain": "nova-brands.com", "brand_id": "glow"},
                "operator": "pinnacle-media.com",
                "billing": "operator",
            },
        ],
    )

    if hasattr(result, 'errors') and result.errors:
        raise Exception(f"Operation failed: {result.errors}")

    for account in result.accounts:
        if account.status == 'active':
            print(f"Ready: {account.brand['domain']}/{account.brand.get('brand_id')} → {account.status}")
        elif account.status == 'pending_approval':
            print(f"Setup required for {account.brand.get('brand_id')}: {account.setup.url}")
            # Poll list_accounts until status becomes active

asyncio.run(main())
```

### Direct brand purchase

```javascript JavaScript theme={null}
import { testAgent } from "@adcp/sdk/testing";
import { SyncAccountsResponseSchema } from "@adcp/sdk";

const result = await testAgent.syncAccounts({
  accounts: [
    {
      brand: { domain: "acme-corp.com" },
      operator: "acme-corp.com",
      billing: "operator",
    },
  ],
});

if (!result.success) {
  throw new Error(`Request failed: ${result.error}`);
}

const validated = SyncAccountsResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

const account = validated.accounts[0];
if (account.status === "active") {
  console.log(`Ready: ${account.brand.domain} — ${account.status}`);
} else if (account.status === "pending_approval") {
  console.log(`Setup required: ${account.setup?.url}`);
  // Poll list_accounts until status becomes active
}
```

```python Python theme={null}
import asyncio
from adcp.testing import test_agent

async def main():
    result = await test_agent.simple.sync_accounts(
        accounts=[
            {
                "brand": {"domain": "acme-corp.com"},
                "operator": "acme-corp.com",
                "billing": "operator",
            },
        ],
    )

    if hasattr(result, 'errors') and result.errors:
        raise Exception(f"Operation failed: {result.errors}")

    account = result.accounts[0]
    if account.status == 'active':
        print(f"Ready: {account.brand['domain']} — {account.status}")
    elif account.status == 'pending_approval':
        print(f"Setup required: {account.setup.url}")
        # Poll list_accounts until status becomes active

asyncio.run(main())
```

### Handling rejection

When a seller declines a request, the account entry has `status: "rejected"`:

```javascript JavaScript theme={null}
import { testAgent } from "@adcp/sdk/testing";
import { SyncAccountsResponseSchema } from "@adcp/sdk";

const result = await testAgent.syncAccounts({
  accounts: [
    {
      brand: { domain: "acme-corp.com", brand_id: "clearance" },
      operator: "acme-corp.com",
    },
  ],
});

if (!result.success) {
  throw new Error(`Request failed: ${result.error}`);
}

const validated = SyncAccountsResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

for (const account of validated.accounts) {
  if (account.status === "rejected") {
    console.log("Account request was rejected");
    if (account.warnings?.length) {
      console.log(`Reason: ${account.warnings.join(", ")}`);
    }
  }
}
```

```python Python theme={null}
import asyncio
from adcp.testing import test_agent

async def main():
    result = await test_agent.simple.sync_accounts(
        accounts=[
            {
                "brand": {"domain": "acme-corp.com", "brand_id": "clearance"},
                "operator": "acme-corp.com",
            },
        ],
    )

    if hasattr(result, 'errors') and result.errors:
        raise Exception(f"Operation failed: {result.errors}")

    for account in result.accounts:
        if account.status == 'rejected':
            print("Account request was rejected")
            warnings = getattr(account, 'warnings', None)
            if warnings:
                print(f"Reason: {', '.join(warnings)}")

asyncio.run(main())
```

## Error handling

| Error Code | Description | Resolution |
| - | - | - |
| [`ACCOUNT_NOT_FOUND`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-account-not-found) | Referenced account does not exist or is not accessible | Check `account_id` or re-sync |
| [`ACCOUNT_AMBIGUOUS`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-account-ambiguous) | A natural-key `account` matches more than one account | Retry with an `account_id` from `error.details.candidates` or [`list_accounts`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/list_accounts) |
| [`ACCOUNT_MOVED`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-account-moved) | **Deprecated in 3.3.** A former natural key was used after a deprecated `operator_identity` rekey | Replace the stale reference with `error.details.current_account`, refresh through [`list_accounts`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/list_accounts), and retry with a fresh idempotency key |
| [`CONFLICT`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-conflict) | Submitted `revision` is stale | Re-read `list_accounts`, reconcile, and retry with the current revision |
| [`ACCOUNT_IDENTITY_CONFLICT`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-account-identity-conflict) | **Deprecated in 3.3.** A deprecated `operator_identity` change targets another account's natural key | Choose a different operator identity or retain the current identity; do not retry the same desired identity unchanged |
| [`UNSUPPORTED_FEATURE`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-unsupported-feature) | Seller does not support changing the requested attribute, or does not advertise the deprecated `operator_identity` change | Leave the attribute unchanged, or open a separate account with the attribute you need |
| [`BILLING_NOT_SUPPORTED`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-billing-not-supported) | Seller-wide capability gate (`supported_billing` does not include the value) or per-account-relationship gate; see [Billing and Account Setup](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/by-layer/L3/error-handling.md#billing-and-account-setup) | Check [`get_adcp_capabilities`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/protocol/get_adcp_capabilities) for `supported_billing`, adjust or omit `billing`; inspect `error.details.scope` to disambiguate capability vs account scope |
| [`BILLING_NOT_PERMITTED_FOR_AGENT`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-billing-not-permitted-for-agent) | Seller-wide capability accepts the value, but the calling buyer agent's commercial relationship does not (e.g., passthrough-only — no payments relationship); see [Buyer-agent identity](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/by-layer/L2/accounts-and-agents.md#buyer-agent-identity) | Retry with `error.details.suggested_billing` (typically `operator`) when present; when absent, surface to a human — the agent cannot extend its own commercial relationship |
| [`PAYMENT_TERMS_NOT_SUPPORTED`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-payment-terms-not-supported) | Seller does not accept the requested payment terms | Omit `payment_terms` to accept the seller's default, or negotiate offline |
| [`ACCOUNT_PAYMENT_REQUIRED`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-account-payment-required) | Account has an outstanding balance requiring payment | Resolve outstanding balance or route to another account |
| [`ACCOUNT_SUSPENDED`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-account-suspended) | Account is suspended | Contact seller to resolve |
| [`BRAND_REQUIRED`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/verification/compliance-catalog#error-code-brand-required) | Billable operation attempted without brand reference | Include `brand` in the request |

## Next steps

* [list\_accounts](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/list_accounts.md) -- Poll for status changes on pending accounts
* [sync\_governance](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/accounts/tasks/sync_governance.md) -- Sync governance agents to accounts
* [Accounts and agents](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/by-layer/L2/accounts-and-agents.md) -- Billing models, trust models, and authorized operators
* [Brand protocol](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/brand-protocol/brand-json.md) -- How seller agents resolve brand identity from the `brand.domain`
* [get\_adcp\_capabilities](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/protocol/get_adcp_capabilities.md) -- Discover `supported_billing` and `require_operator_auth` before syncing accounts


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