Skip to main content
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 buyer-declared account’s natural key describes the advertiser object the seller should provision: a brand, the countries where that advertiser identity applies, the operator, an optional operator-owned unit, an optional fixed currency, an optional buyer-selected immutable timezone, and whether the account is a sandbox. 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) and use natural keys (brand + operator) on subsequent requests. Sellers MAY echo an account_id as an internal handle, but they MUST continue accepting the natural-key AccountRef for accounts provisioned this way. For account-id namespaces, discover seller-assigned account IDs via 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. 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.2.0-beta.0/account/sync-accounts-request.json Response Schema: /schemas/3.2.0-beta.0/account/sync-accounts-response.json

Quick start

Sync a single advertiser account and check the resulting status:

Request parameters

Account entry fields: Natural key: The tuple (brand, operator, operator_unit.id, currency, timezone, sandbox) identifies the advertiser object in the seller system, with currency present only for fixed-currency accounts and timezone present only for buyer-selected account-fixed timezones. Within brand, only domain, brand_id, and canonicalized countries[] participate. Country order has no semantic meaning; send unique uppercase codes in lexical order for stable keys and signatures. operator_unit.name and all mutable BrandRef fields are outside identity, so changing display or per-call metadata does not create a new account. For example, the following key identifies Nova’s NL/DE advertiser object, operated through Pinnacle’s EMEA seat and fixed to EUR. Adding sandbox: true identifies a separate test account.
The natural key is the buyer-declared account reference. A numeric or opaque account_id is instead a seller/storefront identifier: discover it with list_accounts (or receive it out of band) and do not substitute an operator’s buying-platform account ID into that field.

Reconciling operator identity

When a buyer discovers that an existing account has the wrong operator domain or operator unit, it MUST identify the existing account through settings-update mode. Sending the corrected natural key as a new provisioning entry does not communicate replacement intent and can legitimately identify a second account. operator_identity is a complete desired value, not a patch:
  • Omit operator_identity to leave the operator identity unchanged.
  • Include the same operator and operator_unit.id with a different name to update display metadata.
  • Omit operator_unit inside operator_identity to remove the current unit.
  • Change operator_unit.id, add a unit, or remove one to rekey the same account within the operator.
  • Change operator to request an inter-entity operator-domain handoff. The seller MUST place this transition into approval; it cannot apply directly from buyer assertion alone. Before applying it, the seller MUST verify that the authenticated caller can administer the current account, obtain verified brand authorization for the destination operator, and confirm that the destination operator accepts the handoff. A passive or caller-supplied authorized_operators value is not sufficient without the seller’s normal verification of the brand-controlled source.
Identity reconciliation changes the same advertiser account; it is never provisioning or merging. The seller MUST resolve exactly one existing account, reject a target-key collision with ACCOUNT_IDENTITY_CONFLICT, preserve account_id, historical media buys, reporting continuity, and other account-scoped history, and apply no identity change if it cannot preserve that continuity. currency, sandbox, and brand identity remain immutable through this field. An intra-operator unit change stays within the existing account authority boundary. An operator-domain handoff crosses that boundary and therefore has additional rules:
  • The canonical operator, billing, and grants remain unchanged while approval is pending.
  • When billing: "operator", the request MUST include a complete destination_billing_entity. This staged value is write-only while approval is pending; the account response continues to return the current canonical billing_entity. The seller MUST re-run its commercial approval for the destination entity, payment terms, and credit state, then switch canonical operator and billing identity together. It MUST NOT expose or apply the destination billing identity early or silently associate the former operator’s billing identity with the destination.
  • At the atomic switch, grants scoped to the former operator MUST be revoked unless independently authorized for the continuing principal. The destination operator receives only explicitly approved grants. Brand- or buyer-agent grants that remain independently valid MAY be preserved.
  • A seller that cannot complete those transitions without breaking account or resource continuity MUST reject or keep the request pending; it MUST NOT partially rekey the account.
Every persisted account mutation increments its integer revision. Buyers MUST pass the latest observed revision with operator_identity; the seller MUST compare it atomically with the write and return CONFLICT without side effects on mismatch. Dry runs, validation failures, reads, and exact idempotency replays do not increment it. An immediately applied intra-operator update returns the new canonical operator and operator_unit. An operator-domain change always returns the current canonical fields plus identity_change.status: "pending_approval"; list_accounts exposes the same state. Approval atomically performs the authorized identity, billing, and grant transition and clears identity_change. Rejection retains the canonical fields, discards the staged destination billing identity, and exposes identity_change.status: "rejected" with a reason. Each disposition increments revision. A new operator_identity request submitted with the current revision replaces an earlier pending or rejected request and increments the revision. To cancel a pending request without changing the canonical identity, submit the current canonical operator_identity with the current revision; the seller clears identity_change, discards any staged destination billing identity, and increments the revision. A stale request returns CONFLICT without replacing or clearing the existing state. Exact idempotency replays return the original result without another revision increment. After a rekey, the seller MUST tombstone the former natural key while the account or any account-scoped historical resource is retained. An authorized caller using that former key receives ACCOUNT_MOVED with details.current_account; the seller MUST NOT provision a second account from it. Callers without access to the current account receive ACCOUNT_NOT_FOUND so the redirect does not become a cross-tenant existence oracle. With dry_run: true, the seller performs the same account resolution, revision, collision, authorization, and continuity checks but persists nothing and leaves the revision unchanged. Each identity-update result includes identity_change_preview:
  • would_apply means an intra-operator update can apply immediately.
  • would_require_approval means an operator-domain handoff would enter approval.
  • blocked identifies continuity, authorization, billing, grant, or active-resource blockers in blockers.
The preview’s impacts[] records whether account ID, media buys, reporting, approval, billing, and grants would be preserved, revalidated, revoked and regranted, or block the change. A natural-key collision returns ACCOUNT_IDENTITY_CONFLICT and a blocked preview. Dry-run results may use action: "updated" to describe the change that would occur or action: "failed" for a blocker; the top-level dry_run: true and identity_change_preview make clear that no mutation occurred. Dry runs MUST NOT return a persisted identity_change.

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:

Account status

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 the (brand, operator) natural key so the buyer can correlate it to the original sync request. When the seller also returns a seller-assigned account_id, the notification includes it as a convenience handle; buyers still follow the seller’s declared account-reference model for subsequent calls.
If the buyer did not provide push_notification_config, poll 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 provisions or upserts accounts. Used for buyer-declared accounts (require_operator_auth: false). This is the shape AdCP 3.0 shipped with. Sellers MAY echo an account_id, but the natural-key AccountRef remains valid for subsequent calls.
  • Settings-update modeaccount (an AccountRef) at the entry root, with brand/operator/billing absent. The seller updates the account’s settable state — no provisioning side effects. Used for account-id namespaces only when the seller exposes settings updates through this task; upstream-managed accounts are discovered via list_accounts, while seller-defined account IDs may be supplied out-of-band. Buyer-declared account sellers MAY also accept this mode for settings updates against accounts they previously provisioned.
Schema enforces the exclusivity via oneOf — sending both shapes on the same entry is a validation error. Sellers that don’t implement settings-update mode reject account-keyed entries with UNSUPPORTED_PROVISIONING; sellers that don’t provision through sync_accounts, including account-id namespaces, reject natural-key provisioning entries with the same code.

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 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 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 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. For these event types, “wholesale feed” means the seller’s buyable wholesale product and signals feeds returned by get_products or get_signals; it is not the buyer-provided feeds managed by 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, product.created, product.updated, product.priced, product.removed, signal.created, signal.updated, signal.priced, signal.removed, wholesale_feed.bulk_change). Sellers MUST reject any media-buy-anchored type (scheduled, final, delayed, adjusted, window_update, impairment) or agent-anchored type (capabilities.changed) as a per-account validation failure with INVALID_REQUEST or VALIDATION_ERROR in accounts[].errors[], and error.field MUST point at the invalid event_types entry.
  • product_payload_viewcanonical for a list_products mirror or legacy for the 3.x get_products shape. Omission defaults to legacy; valid only when a product event is selected.
  • authentication (optional) — legacy Bearer or HMAC-SHA256. Omit to use the default RFC 9421 webhook profile. When present, the same signed-registration downgrade-resistance rules as push_notification_config.authentication apply. Credentials are write-only — sellers omit them on reads.
  • active (default true) — set false to pause a subscriber without removing the registration. Sellers MAY skip only the outbound proof challenge while active: false; they MUST still enforce HTTPS parsing, hostname normalization, and reserved-range rejection on write. Paused subscribers MUST NOT receive fires until reactivated. Reactivation MUST repeat full SSRF validation with connect pinning plus proof-of-control for any tuple without current valid proof.

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, 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 and webhook-challenge-response.json.
The receiver proves control by returning HTTP 2xx with a JSON body containing exactly one echo field:
Sellers MUST also accept the backward-compatible alias:
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.
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 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:
Example — register a wholesale feed mirror subscriber for wholesale product and signal changes:
Example — register an account lifecycle subscriber:
Governance agents registered via 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 — 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: 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 for capability declaration and event semantics.

Common scenarios

Agency syncing multiple brands

Direct brand purchase

Handling rejection

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

Error handling

Next steps