{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/account/sync-accounts-request.json",
  "title": "Sync Accounts Request",
  "description": "Sync advertiser account state with a seller. Two modes, distinguished by the key on each per-account entry:\n\n- **Provisioning mode** (`brand` + `operator` + `billing` at the entry root): the agent declares the advertiser identity, operator, optional operator-owned buying unit, optional fixed account currency, conditionally required buyer-selected account timezone, sandbox disposition, and billing model. The seller provisions or links the corresponding advertiser object via upsert. `brand.countries`, `operator_unit.id`, `currency`, buyer-selected `timezone`, and `sandbox` participate in the buyer-declared natural key when present; `operator_unit.name` is display metadata only. The seller MAY echo a seller-assigned account_id but MUST continue accepting the complete natural-key AccountRef.\n\n- **Settings-update mode** (`account` field carrying an [`AccountRef`](/schemas/core/account-ref.json)): targets an existing account by seller/storefront `account_id` or buyer-declared natural key. The seller updates settable state without provisioning side effects. A complete `operator_identity` value reconciles the existing account to the buyer's desired operator domain and optional operator unit; omission leaves identity unchanged.\n\nExactly one key shape is allowed per entry. Sellers that do not implement one mode return `UNSUPPORTED_PROVISIONING` for that mode. Identity reconciliation is additionally gated by `get_adcp_capabilities.account.identity_updates`.",
  "x-tool-summary": "Provision advertiser accounts or update settings for existing accounts through declarative synchronization.",
  "type": "object",
  "allOf": [
    {
      "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/version-envelope.json"
    }
  ],
  "x-mutates-state": true,
  "properties": {
    "idempotency_key": {
      "type": "string",
      "description": "Client-generated unique key for at-most-once execution. Natural per-account upsert keys handle resource-level dedup, but the envelope triggers onboarding webhooks, billing setup, and audit events — this key prevents those side effects from firing twice on retry. MUST be unique per (seller, request) pair. Use a fresh UUID v4 for each request.",
      "minLength": 16,
      "maxLength": 255,
      "pattern": "^[A-Za-z0-9_.:-]{16,255}$"
    },
    "accounts": {
      "type": "array",
      "description": "Per-account sync entries. Each entry uses one of two key shapes: the `account` field (AccountRef) for settings-update mode, or the flat `brand` + `operator` + `billing` trio for provisioning mode. An operator_identity settings update MUST carry the latest account revision.",
      "items": {
        "type": "object",
        "description": "An advertiser account entry — either provisions/upserts a new account (natural key) or updates an existing one (AccountRef key).",
        "properties": {
          "account": {
            "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/account-ref.json",
            "description": "Settings-update key. When present, this entry targets an existing account by `account_id` (seller-owned account namespace) or natural key (buyer-declared account settings-update against a previously-provisioned account). Mutually exclusive with the flat `brand` + `operator` + `billing` provisioning trio. When `account` is present, the seller MUST NOT create a new account — entries that would otherwise trigger provisioning are rejected with `UNSUPPORTED_PROVISIONING`."
          },
          "revision": {
            "type": "integer",
            "minimum": 1,
            "description": "Expected current account revision for optimistic concurrency in settings-update mode. Required whenever operator_identity is present; optional for existing non-identity settings updates. The seller MUST compare it atomically with the write, reject a mismatch with CONFLICT, and leave the account unchanged. Obtain it from list_accounts or the most recent sync_accounts result. Reads, dry runs, validation failures, and exact idempotency replays do not increment revision; every persisted settings or identity-change state transition does. MUST be absent in provisioning mode."
          },
          "operator_identity": {
            "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/operator-identity.json",
            "description": "Complete desired operator identity for settings-update mode. Omit this field to leave operator identity unchanged. When present, omission of operator_unit within the object removes the existing unit. Changing only operator_unit.name updates display metadata; changing operator_unit.id or adding/removing a unit rekeys the same account within the current operator. Changing operator requests an inter-entity handoff and MUST enter pending_approval until the seller verifies the current account authority, verified brand authorization, destination-operator acceptance, and any operator-scoped billing and grant transition. The seller MUST preserve account_id and account-scoped historical resources, MUST reject collisions without merging, and MUST apply no identity change if continuity cannot be preserved. MUST be accompanied by revision and MUST be absent in provisioning mode."
          },
          "destination_billing_entity": {
            "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/business-entity.json",
            "description": "Complete staged billing identity for the requested destination operator during an operator-domain handoff on an account whose billing party is operator. This value is write-only while approval is pending and MUST NOT replace or be echoed as the account's canonical billing_entity until the handoff applies atomically. Required by the protocol when an operator-billed account changes operator; otherwise MUST be absent. Requires operator_identity and revision and MUST be absent in provisioning mode."
          },
          "brand": {
            "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/brand-ref.json",
            "description": "Brand reference identifying the advertiser. Required for **provisioning mode**; MUST be absent in settings-update mode. Only the BrandKey projection — `domain`, `brand_id`, and the canonicalized `countries[]` set — participates in account identity. Mutable or per-call BrandRef fields such as `industries`, `data_subject_contestation`, and `brand_kit_override` MUST NOT affect lookup, idempotency, or account creation. New 3.2 producers SHOULD send only the BrandKey fields; the broader BrandRef remains accepted on this existing 3.x task for compatibility."
          },
          "operator": {
            "type": "string",
            "description": "Domain of the entity operating on the brand's behalf (e.g., 'pinnacle-media.com'). When the brand operates directly, this is the brand's domain. Verified against the brand's authorized_operators in brand.json. Required for **provisioning mode**; MUST be absent in settings-update mode.",
            "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$"
          },
          "operator_unit": {
            "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/operator-unit.json",
            "description": "Optional operator-owned business unit, agency seat, or platform account for provisioning mode. operator_unit.id participates in the natural key; name is mapping/display metadata. MUST be absent in settings-update mode."
          },
          "currency": {
            "type": "string",
            "pattern": "^[A-Z]{3}$",
            "description": "Optional immutable ISO 4217 transaction currency for a currency-bound advertiser object. Consult `account.supported_account_currency_modes` before provisioning. When supplied, it participates in the natural key and all media buys on the account use it. Omit for per-media-buy currency selection. MUST be absent in settings-update mode."
          },
          "timezone": {
            "type": "string",
            "minLength": 1,
            "description": "Immutable operational timezone selected for an account_fixed advertiser object. Required in provisioning mode when get_adcp_capabilities.account.timezone declares account_selection: buyer_selected, and the value MUST be one of supported_timezones. Omit for seller_fixed or seller_assigned modes. When supplied, it participates in the natural key. MUST be absent in settings-update mode."
          },
          "billing": {
            "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/enums/billing-party.json",
            "description": "Who the seller invoices for this buyer–storefront account relationship. Required for **provisioning mode**; MUST be absent in settings-update mode (the invoiced party is fixed at provisioning time and cannot be changed via settings-update). This field does not select a payment rail, clearing intermediary, or per-media-buy settlement route."
          },
          "billing_entity": {
            "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/business-entity.json",
            "description": "Business entity details for the party responsible for payment. The agent provides this so the seller has the legal name, tax IDs, address, and bank details needed for formal B2B invoicing. Permitted in both modes — sellers MAY accept refinements in settings-update mode (e.g., updated bank details)."
          },
          "payment_terms": {
            "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/enums/payment-terms.json",
            "description": "Payment terms for this account. The seller must either accept these terms or reject the account — terms are never silently remapped. When omitted, the seller applies its default terms. Permitted in both modes."
          },
          "sandbox": {
            "type": "boolean",
            "description": "When true, provision this as a sandbox account with no real platform calls or billing. Only applicable to buyer-declared accounts (require_operator_auth: false) in provisioning mode. For account-id namespaces, sandbox accounts are pre-existing test accounts discovered via list_accounts or supplied out-of-band."
          },
          "preferred_reporting_protocol": {
            "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/enums/cloud-storage-protocol.json",
            "description": "Buyer's preferred cloud storage protocol for offline reporting delivery. The seller provisions the account's reporting_bucket using this protocol if supported. When omitted, the seller chooses from its supported offline_delivery_protocols. Only meaningful when the seller's reporting_delivery_methods includes 'offline'."
          },
          "notification_configs": {
            "type": "array",
            "description": "Account-level webhook subscriptions for notifications whose lifecycle outlives any single media buy (`creative.status_changed`, optional `creative.assignment_changed`, `indicators.changed`, `creative.purged`, `account.status_changed`, wholesale feed change payloads, and future account-anchored resource events after those event types are added to `notification-config.json`). Indicator and assignment registrations are prospective: activation does not replay current conditions, so buyers establish a complete baseline through `get_media_buys` by enumerating known IDs or requesting every status and exhausting pagination, without an indicator filter. Durable account lifecycle transitions such as later `payment_required`, `suspended`, `closed`, or recovery to `active` use `account.status_changed` on this surface; the one-shot `sync_accounts.push_notification_config` channel remains scoped to the async result of the original provisioning task. Declarative replace semantics: when this field is present, the buyer sends the full desired array and the seller replaces the account's current set with that array, keyed by account-scoped `subscriber_id`. Omit this field to leave existing subscribers unchanged; send `[]` to remove all subscribers. Re-sending an existing `subscriber_id` for the account replaces that subscriber's config rather than creating a duplicate; persisted entries whose `subscriber_id` does not appear in the sent array are removed, so the seller MUST NOT merge the new array with persisted state. Paused entries (`active: false`) use the same replacement semantics; a buyer that wants to preserve a paused subscriber MUST re-include it with `active: false`. Duplicate `subscriber_id` values within one submitted array are rejected. Permitted in both provisioning and settings-update modes. Each entry registers a URL, the event types the subscriber wants, and optional legacy auth — see [`notification-config.json`](/schemas/core/notification-config.json). The seller MUST echo applied state on the response and on `list_accounts` reads, with `authentication.credentials` omitted (write-only). Sellers MUST reject entries whose `event_types` include any type whose contract anchors at a media buy or below (today: `scheduled`, `final`, `delayed`, `adjusted`, `window_update`, `impairment`) or at the agent (today: `capabilities.changed`) as per-account validation failures with `INVALID_REQUEST` or `VALIDATION_ERROR` and `error.field` pointing at the invalid `event_types` entry — those events do not belong on this surface. Wholesale feed webhook registrations carry the actual change payload in `/schemas/core/wholesale-feed-webhook.json`; canonical product subscribers repair through `list_products(if_feed_version)`, legacy product subscribers through `get_products(if_wholesale_feed_version)`, and signal subscribers through `get_signals(if_wholesale_feed_version)`. Account status change registrations carry the invalidation payload in `/schemas/core/account-status-changed-webhook.json`; receivers use `list_accounts` to repair or reconcile. This is distinct from sync_catalogs, which manages buyer-provided campaign input feeds on a seller account.\n\nActivation proof: before activating a new or changed active subscriber, the seller MUST validate the URL, complete the account-level webhook proof-of-control challenge, and only then persist or expose the subscriber as `active: true`. For `account.status_changed`, sellers MUST assign `account_id` before completing proof so subsequent status transitions can identify the account and be repaired through `list_accounts`, even when external approval remains pending. A valid existing proof for the same `(account_id, subscriber_id, normalized url, authentication mode/credential binding, normalized event_types)` tuple MAY be reused; changing any element of that tuple requires fresh proof. The challenge POST itself MUST be signed with the seller's RFC 9421 webhook profile key and MUST include seller_agent_url, delivery_auth, and event_types so the receiver can verify the pending registration before echoing the challenge. New signers use `adcp_use: \"request-signing\"`; deprecated `webhook-signing` keys remain accepted during the compatibility window. Entries sent with `active: false` may skip only the outbound proof challenge while inactive; sellers MUST still enforce URL parsing, HTTPS, hostname normalization, and reserved-range rejection at write time, and those entries MUST NOT receive fires until reactivated. If proof fails or times out, the seller rejects the account entry with `action: \"failed\"`, leaves the prior notification_configs[] set unchanged, and reports `VALIDATION_ERROR` (or `INVALID_REQUEST` for malformed URLs) at the failing `notification_configs[j].url` field.\n\n**Cap rationale:** `maxItems: 16` is a practical fan-out cap (governance + buyer ingestion + audit bus + dx team + a few partner hooks). The cap exists to prevent unbounded subscriber arrays in storage and to bound the seller's per-event fan-out work. Sellers that hit the cap with legitimate subscribers should surface this on the protocol roadmap rather than work around it.",
            "items": {
              "allOf": [
                {
                  "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/notification-config.json"
                },
                {
                  "if": {
                    "required": [
                      "authentication"
                    ]
                  },
                  "then": {
                    "properties": {
                      "authentication": {
                        "required": [
                          "credentials"
                        ]
                      }
                    }
                  }
                }
              ]
            },
            "maxItems": 16
          }
        },
        "oneOf": [
          {
            "title": "ProvisioningMode",
            "description": "Provisioning-mode entry — natural-key trio is required; account, revision, and operator_identity are forbidden.",
            "required": [
              "brand",
              "operator",
              "billing"
            ],
            "allOf": [
              {
                "not": {
                  "required": [
                    "account"
                  ]
                }
              },
              {
                "not": {
                  "required": [
                    "revision"
                  ]
                }
              },
              {
                "not": {
                  "required": [
                    "operator_identity"
                  ]
                }
              },
              {
                "not": {
                  "required": [
                    "destination_billing_entity"
                  ]
                }
              }
            ]
          },
          {
            "title": "SettingsUpdateMode",
            "description": "Settings-update entry — `account` (AccountRef) is required, provisioning trio fields are forbidden.",
            "required": [
              "account"
            ],
            "allOf": [
              {
                "not": {
                  "required": [
                    "brand"
                  ]
                }
              },
              {
                "not": {
                  "required": [
                    "operator"
                  ]
                }
              },
              {
                "not": {
                  "required": [
                    "operator_unit"
                  ]
                }
              },
              {
                "not": {
                  "required": [
                    "currency"
                  ]
                }
              },
              {
                "not": {
                  "required": [
                    "timezone"
                  ]
                }
              },
              {
                "not": {
                  "required": [
                    "billing"
                  ]
                }
              }
            ]
          }
        ],
        "dependencies": {
          "operator_identity": [
            "revision"
          ],
          "destination_billing_entity": [
            "operator_identity",
            "revision"
          ]
        },
        "additionalProperties": true
      },
      "maxItems": 1000
    },
    "delete_missing": {
      "type": "boolean",
      "default": false,
      "description": "When true, accounts previously synced by this agent but not included in this request will be deactivated. Scoped to the authenticated agent — does not affect accounts managed by other agents. Use with caution."
    },
    "dry_run": {
      "type": "boolean",
      "default": false,
      "description": "When true, preview what would change without applying. Returns what would be created/updated/deactivated."
    },
    "push_notification_config": {
      "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/push-notification-config.json",
      "description": "Webhook for async notifications when account status changes (e.g., pending_approval transitions to active)."
    },
    "context": {
      "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/context.json"
    },
    "ext": {
      "$ref": "https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/ext.json"
    }
  },
  "required": [
    "idempotency_key",
    "accounts"
  ],
  "additionalProperties": true,
  "examples": [
    {
      "description": "Agency syncing multiple advertisers with different billing",
      "data": {
        "idempotency_key": "a7f9c2e4-1234-4567-89ab-cdef01234567",
        "accounts": [
          {
            "brand": {
              "domain": "nova-brands.com",
              "brand_id": "spark",
              "countries": [
                "DE",
                "NL"
              ]
            },
            "operator": "pinnacle-media.com",
            "operator_unit": {
              "id": "seat_emea_01",
              "name": "EMEA"
            },
            "currency": "EUR",
            "billing": "operator"
          },
          {
            "brand": {
              "domain": "nova-brands.com",
              "brand_id": "glow"
            },
            "operator": "pinnacle-media.com",
            "billing": "agent"
          }
        ]
      }
    },
    {
      "description": "Brand buying direct with payment terms",
      "data": {
        "idempotency_key": "b8e0d3f5-2345-4678-9abc-def012345678",
        "accounts": [
          {
            "brand": {
              "domain": "acme-corp.com"
            },
            "operator": "acme-corp.com",
            "billing": "operator",
            "payment_terms": "net_30"
          }
        ]
      }
    },
    {
      "description": "Agent consolidating billing with net-60 terms",
      "data": {
        "idempotency_key": "c9f1e4a6-3456-4789-abcd-ef0123456789",
        "accounts": [
          {
            "brand": {
              "domain": "nova-brands.com",
              "brand_id": "spark"
            },
            "operator": "pinnacle-media.com",
            "billing": "agent",
            "payment_terms": "net_60"
          },
          {
            "brand": {
              "domain": "nova-brands.com",
              "brand_id": "glow"
            },
            "operator": "pinnacle-media.com",
            "billing": "agent",
            "payment_terms": "net_60"
          }
        ]
      }
    },
    {
      "description": "Advertiser billed directly with structured billing entity (DACH B2B)",
      "data": {
        "idempotency_key": "d0a2f5b7-4567-489a-bcde-f01234567890",
        "accounts": [
          {
            "brand": {
              "domain": "acme-corp.com"
            },
            "operator": "pinnacle-media.com",
            "billing": "advertiser",
            "billing_entity": {
              "legal_name": "Acme Corporation GmbH",
              "vat_id": "DE987654321",
              "registration_number": "HRB 67890",
              "address": {
                "street": "Hauptstrasse 42",
                "city": "Munich",
                "postal_code": "80331",
                "country": "DE"
              },
              "contacts": [
                {
                  "role": "billing",
                  "name": "AP Department",
                  "email": "billing@acme-corp.com"
                }
              ],
              "bank": {
                "account_holder": "Acme Corporation GmbH",
                "iban": "DE75512108001245126199",
                "bic": "SOLADEST600"
              }
            },
            "payment_terms": "net_30"
          }
        ]
      }
    },
    {
      "description": "Provisioning mode — register a creative-lifecycle webhook subscription alongside account provisioning",
      "data": {
        "idempotency_key": "e1b3a6c8-5678-49ab-cdef-1234567890ab",
        "accounts": [
          {
            "brand": {
              "domain": "acme-corp.com"
            },
            "operator": "acme-corp.com",
            "billing": "operator",
            "notification_configs": [
              {
                "subscriber_id": "buyer-primary",
                "url": "https://buyer.example/webhooks/adcp/creative",
                "event_types": [
                  "creative.status_changed",
                  "creative.purged"
                ],
                "active": true
              }
            ]
          }
        ]
      }
    },
    {
      "description": "Settings-update mode — register webhook subscribers on an existing account-id namespace account",
      "data": {
        "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
              }
            ]
          }
        ]
      }
    },
    {
      "description": "Settings-update mode — register a wholesale feed mirror webhook subscriber for wholesale product and signal changes",
      "data": {
        "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
              }
            ]
          }
        ]
      }
    }
  ]
}
