Superseded where the seller supports
account.requests (#8543). There, open and change accounts with request_account, read accounts and their requests with 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. 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), 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.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. 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.
When the seller advertises account.requests (experimental in 3.3), prefer 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.
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
Response Schema: /schemas/3.3.0-beta.1/account/sync-accounts-response.json
Quick start
Sync a single advertiser account and check the resulting status:Request parameters
Account entry fields:
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, 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. This is the match the request_account deduplication guidance 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:
account_id. That value belongs in operator_unit.id; account_id is the seller’s identifier.
Updating account attributes
To change an attribute, such asbrand.countries or billing, send a settings-update entry that names the account by account_id. The following entry changes the countries:
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. A seller that does not support changing an attribute rejects the entry with 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:
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.
revision is optional and guards the write. When it is stale, the seller returns 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.
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 and 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. 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 anaccounts 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). Noaccountsarray is present.
accounts OR errors, never both.
Per-account fields:
Account status
Async notifications
Whenpush_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.
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+billingat the entry root. The seller opens an account and MUST return itsaccount_idonce the account is resolved. When the entry matches exactly one account in its account scope, the seller SHOULD return thataccount_idinstead; see Request parameters. Used for buyer-declared accounts (require_operator_auth: false). This is the shape AdCP 3.0 shipped with. The natural-keyAccountRefremains valid for subsequent calls throughout 3.x. - Settings-update mode —
account(an AccountRef) at the entry root, withoperatorabsent. The seller updates the account’s settable state and attributes,brand.countriesandbillingincluded — no provisioning side effects. A seller that opens buyer-declared accounts throughsync_accountsaccepts settings-update entries keyed byaccount_id, and declines an attribute change it cannot make withUNSUPPORTED_FEATURE. For account-id namespaces, the mode applies only when the seller exposes settings updates through this task. Upstream-managed accounts are discovered vialist_accounts, while seller-defined account IDs may be supplied out-of-band.
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. 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:
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.
Account change feed notifications
account.change_recorded is the generic wake-up for the optional durable
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 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 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_configsto 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.
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 endpointurl— 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 experimentalcollection_card.updated,collection_card.removedandcollection_catalog.bulk_change, which a buyer registers only with a seller that lists them inwholesale_feed_webhooks.event_types; seelist_collections). 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 withINVALID_REQUESTorVALIDATION_ERRORinaccounts[].errors[], anderror.fieldMUST point at the invalidevent_typesentry.product_payload_view—canonicalfor alist_productsmirror orlegacyfor the 3.xget_productsshape. Omission defaults tolegacy; 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 withlegacy_hmac_fallback: falserejects an entry that carries it withUNSUPPORTED_FEATUREinaccounts[].errors[]. When present, the same signed-registration downgrade-resistance rules aspush_notification_config.authenticationapply. Credentials are write-only — sellers omit them on reads.active(defaulttrue) — setfalseto pause a subscriber without removing the registration. Sellers MAY skip only the outbound proof challenge whileactive: 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 asactive: 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.
2xx with a JSON body containing exactly one echo field:
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 initialsync_accounts response, sellers fire account.status_changed to active subscribers whose event_types[] includes that value.
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:
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 hasstatus: "rejected":
Error handling
Next steps
- list_accounts — Poll for status changes on pending accounts
- sync_governance — Sync governance agents to accounts
- Accounts and agents — Billing models, trust models, and authorized operators
- Brand protocol — How seller agents resolve brand identity from the
brand.domain - get_adcp_capabilities — Discover
supported_billingandrequire_operator_authbefore syncing accounts