{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "/schemas/3.1.0-rc.4/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 which brands it represents, who operates on each brand's behalf, and the billing model. The seller provisions or links accounts via upsert. Used when brand + operator (+ sandbox) is the durable protocol key for buyer-declared accounts (`require_operator_auth: false`). Sellers MAY echo a seller-assigned account_id, but they MUST continue accepting the natural-key AccountRef for every account provisioned this way.\n\n- **Settings-update mode** (`account` field carrying an [`AccountRef`](/schemas/core/account-ref.json)): targets an existing account by `account_id` (or by natural key for buyer-declared accounts). The seller updates the account's settable state (notification subscriptions, payment terms, billing entity refinements) — no provisioning side effects. Used for account_id namespaces only when the seller exposes this task for account settings; the account_id itself is discovered via `list_accounts` for upstream-managed namespaces or supplied out-of-band for seller-defined namespaces. Buyer-declared account sellers MAY also accept this mode for settings updates against accounts they previously provisioned.\n\nExactly one of the two key shapes is allowed per entry. Sellers that do not implement settings-update mode MUST return `UNSUPPORTED_PROVISIONING` on entries keyed by `account.account_id`; sellers that do not provision through sync_accounts MUST return `UNSUPPORTED_PROVISIONING` on entries keyed by the natural-key trio. Account-id namespace provisioning is out of scope unless a future explicit capability declares it.",
  "type": "object",
  "allOf": [
    {
      "$ref": "/schemas/3.1.0-rc.4/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 (brand, operator) 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.",
      "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": "/schemas/3.1.0-rc.4/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`."
          },
          "brand": {
            "$ref": "/schemas/3.1.0-rc.4/core/brand-ref.json",
            "description": "Brand reference identifying the advertiser. Required for **provisioning mode**; MUST be absent in settings-update mode."
          },
          "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])?)*$"
          },
          "billing": {
            "$ref": "/schemas/3.1.0-rc.4/enums/billing-party.json",
            "description": "Who should be invoiced. Required for **provisioning mode**; MUST be absent in settings-update mode (billing is fixed at provisioning time and cannot be changed via settings-update)."
          },
          "billing_entity": {
            "$ref": "/schemas/3.1.0-rc.4/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": "/schemas/3.1.0-rc.4/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": "/schemas/3.1.0-rc.4/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`, `creative.purged`, wholesale feed change payloads, future account-anchored resource events after those event types are added to `notification-type.json`). This surface does not currently carry lifecycle events for the account object itself (for example, there is no `account.status_changed` event type); account status changes are observed through `list_accounts` polling or the one-shot `sync_accounts.push_notification_config` async result channel. 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`, `impairment`) or account-lifecycle names not present in the enum 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`; receivers use `get_products` / `get_signals` with `if_wholesale_feed_version` 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`. 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-signing key and MUST include seller_agent_url, delivery_auth, and event_types so the receiver can verify the pending registration before echoing the challenge. 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": "/schemas/3.1.0-rc.4/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` is forbidden.",
            "required": [
              "brand",
              "operator",
              "billing"
            ],
            "not": {
              "required": [
                "account"
              ]
            }
          },
          {
            "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": [
                    "billing"
                  ]
                }
              }
            ]
          }
        ],
        "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": "/schemas/3.1.0-rc.4/core/push-notification-config.json",
      "description": "Webhook for async notifications when account status changes (e.g., pending_approval transitions to active)."
    },
    "context": {
      "$ref": "/schemas/3.1.0-rc.4/core/context.json"
    },
    "ext": {
      "$ref": "/schemas/3.1.0-rc.4/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"
            },
            "operator": "pinnacle-media.com",
            "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
              }
            ]
          }
        ]
      }
    }
  ]
}
