sync_principal replaces the principal’s standing configuration with one AdCP seller agent: webhooks, reusable reporting destinations, and its position in capability negotiation. The three tiers of the hierarchy:
get_adcp_capabilities states what the seller can support; sync_principal states which of it this principal uses; account-level tasks authorize work for particular advertisers.
The task does not register a buyer agent and does not accept a buyer-agent URL or identity in its body. Who the principal is comes only from authenticated transport.
Three consequences that are easy to get wrong:
- It is optional. The principal exists as soon as a stable credential authenticates. A buyer that never calls
sync_principalcan still usesync_accounts,list_accounts, and every domain task; it only forgoes principal-wide webhooks, reusable reporting destinations, and declared async versions. - There is one principal per credential, not one per operator. An agent that authenticates as itself is one
buyer_agentprincipal however many operators it acts for; those operators are fields on its accounts. Operators are principals only when they authenticate themselves, which is therequire_operator_auth: truemodel. - It grants no account authority. Accounts are authorized through
sync_accounts,list_accounts, and the seller’s own access controls, never by principal configuration.
/schemas/3.2.1/protocol/sync-principal-request.json
Response schema: /schemas/3.2.1/protocol/sync-principal-response.json
Identity and authority
The principal is a party, in the same sense the protocol already bills and authorizes parties. It comes in two kinds:- A buyer agent authenticating as itself — its registered workload, OAuth client-credentials subject, server-side API-key owner, verified signed-request identity, or equivalent machine principal. The operators an agent acts for are agent-scoped labels within its own principal space, asserted the same way
operator_identityis on accounts; they confer no identity across agents. - An operator-side identity — a person at the operator authenticated as themselves. The client application they act through (Claude, ChatGPT, the seller’s UI) is the instrument, never the owner. A person using ChatGPT therefore configures under their own login at the seller; nothing about ChatGPT is registered.
brand.json agents[] entry—but only after the seller verifies the publication chain and confirms that entry authorizes the request-signing key. The URL is therefore a verified identity input, not a body assertion. The operator domain, raw URL spelling, brand_json_url, jwks_uri, kid, key thumbprint, and key generation remain trust or credential-binding facts beneath that identity. Two canonical Agent URLs are two principals even when they share a domain, operator, type, or JWKS endpoint; key rotation under one canonical Agent URL preserves that principal. If the verified chain cannot select exactly one canonical Agent URL, the seller fails closed without reading or creating principal state.
For a Web Bot Auth signed caller (experimental), the stable Agent subject is the signer’s canonical origin, verified as Web Bot Auth identities describes. One agent occupies one origin, so key rotation within the origin’s directory preserves the principal. Whether a verified origin has a principal, and how it gets one, is first contact and binding.
Every applied, current, or recognized readback carries the seller-resolved principal_kind — buyer_agent or operator — resolved solely from the authorization system, never from request content, so per-party policy such as billing gates can rely on it before configuration begins. A request may carry expected_principal_kind as an assertion fence (“I believe I am authenticating as the agent”); on mismatch with the resolved kind the seller rejects with CONFLICT before mutation. The assertion never influences resolution.
The response’s principal_id is a seller-issued audit and routing handle for the principal record. It is not a credential and does not authorize any advertiser account.
Establishing a connection
The principal layer begins after authentication. AdCP defines no credential-registration protocol: the seller’s authorization system — OAuth, API keys, signed requests, workload identity — issues and maps credentials before the first call. The same lifecycle then works for an autonomous buyer agent, a person in an interactive client, an API-key integration, or M2M OAuth:- Authenticate. Autonomous software presents its workload identity; a person presents their delegated user identity. No
register_buyer_agentstep exists or is needed. - Read
get_adcp_capabilities. Discoveradcp.principal, its supported sections, event and delivery offerings, and limits. - Verify the resolved identity. Call
get_principal. Arecognizedresult returns the existing durableprincipal_idand seller-resolvedprincipal_kindwithout standing configuration.unconfiguredmeans stable authentication succeeded but no durable record exists. The read creates neither records nor identifiers. When the result carriesprincipal_kind(currentorrecognized), buyers MAY use it as a preflight before account calls:buyer_agentmatchesaccount.require_operator_auth: false, andoperatormatchestrue. A mismatch means the buyer presented the wrong credential for this seller’s account model. - Call
sync_principal. Declare consumption facts (declarations), register webhook subscribers (includingprincipal.changedand, with explicit scope, account-anchored events), and register reusable reporting destinations. - Authorize advertiser accounts separately through
request_accountorsync_accountsand account-level tasks. Nothing in steps 1–4 grants account authority. - Bind feeds. Register one reporting subscription per feed purpose that covers every authorized account, or bind each account’s feeds to the reusable
destination_refs throughsync_accounts. - Observe readiness.
get_principalshows destination and declaration state;principal.changedkeeps it fresh.
Discovery
Only call this task whenget_adcp_capabilities includes both:
reporting_destination_offerings is the seller’s objective delivery offering: a buyer selects a compatible pattern, transport, format, access mode, and verification-profile intersection from it before submitting, instead of discovering support through rejections. A destination outside the offering fails the whole request with UNSUPPORTED_FEATURE; a destination inside the offering that fails provider-side setup is reported per destination as validating, action_required, or rejected inside an applied response.
Synchronization semantics
configuration contains one or more independently replaceable sections:
For each submitted section:
- The array is complete desired state for that section.
- Omitting a previously present entry removes it; for reporting destinations, removal is revocation.
[]clears the whole section.- An omitted section remains unchanged.
- All submitted sections apply atomically, or none do. Sections are validated and authorized in full before any proof challenge or grant is issued, so a failed request leaves no external side effects.
dry_run: truereturnsresult.kind: validatedwith the would-be action, without persistence, grants, endpoint challenges, principal IDs, destination references, or configuration versions. Dry-run outcomes and warnings are computed solely against the caller’s own state and never reveal what other principals have registered.
expected_configuration_version is present, the seller applies a new operation only if it equals the current opaque version. A stale version returns CONFLICT without mutation. This prevents two services using the same principal from silently overwriting each other. Read the current version with get_principal before a guarded replacement — never write blind to discover it. Retries of one logical request reuse both its idempotency_key and body. The seller resolves an exact idempotent replay before evaluating the version fence, so a lost successful response remains replayable after the persisted version advances. Idempotency records are scoped to the stable principal; a key first seen under a different principal starts a new operation, never a replay.
configuration_version advances on every successful mutation of the shared underlying state — through this task or through sync_agent_notification_configs — and never on seller-driven setup-state transitions such as a destination becoming ready.
principal-level webhooks
One subscriber registered here can carry every webhook the relationship needs — the “sign me up for everything, once” surface:- Caller-anchored events (
capabilities.changed,principal.changed) fire principal-wide.principal.changedis the invalidation doorbell for seller-driven state transitions — a destination reachingready, a setup action expiring, the accepted declarations intersection changing, a relationship status change or approaching expiry, a reporting subscription’saccounts_unauthorizedchanging, an account’s coverage exception opening or closing, or the subscription moving toaction_required— and receivers repair by re-readingget_principal. - Account-anchored events (
creative.status_changed,account.change_recorded,product.*, …) may be listed on caller-level subscribers, and require the explicitall_authorized_accounts: trueacknowledgment — subscribing an endpoint to every account is never implicit. Authorization is evaluated at each delivery attempt, including retries: losing authority for an account both stops new fires and suppresses queued retries carrying that account’s data, without touching the subscription. Registration itself requires no account authority and grants none. - Account lifecycle. A caller-level subscriber whose
event_typesincludeaccount.status_changed, withall_authorized_accounts: true, receives every authorized account’s status transitions, such assuspendedorpayment_required, from one registration. Sellers that support this listaccount.status_changedinprincipal.caller_event_types. - Media-buy-anchored cadence reports (
scheduled,final,delayed,adjusted,window_update) stay on each buy’sreporting_webhook— they carry per-buy cadence choices this layer deliberately does not own.impairmentis also media-buy-anchored but fires againstpush_notification_config— it is a dependency-state change on the buy, not a cadence report.
event_types and may set include_future_event_types: true — which extends only to future caller-eligible types classified invalidation-only (identifiers plus a repair pointer, no domain data). Payload-bearing types always require explicit enumeration, so the flag can never silently opt a caller into more-sensitive payloads. Sellers advertise which types they can emit here in principal.caller_event_types; an out-of-offering type fails the request with UNSUPPORTED_FEATURE.
Caller-level and account-level subscriptions to the same event are independent: both fire, and receivers dedupe by the event’s logical notification_id. Setting a subscriber’s active: false is the webhook kill switch — all fires to that endpoint, across every account, halt within the advertised suspension_interval_seconds.
Declarations
Synchronous calls negotiate versions per request, but a webhook fired weeks after registration has no request context to negotiate against. Thedeclarations section is that channel: the caller declares bounded facts about what it can consume, and the seller returns the accepted intersection it will honor for asynchronous interactions.
- Declarations are facts about consumption ability — never preferences or policy.
get_adcp_capabilitiesstays objective seller facts; the intersection is the negotiated contract. - Unknown
experimental_featuresidentifiers are accepted and simply excluded from the intersection, so one declaration works across sellers with different surfaces. - An empty
webhook_signing_algorithmsintersection while any webhook subscriber is active fails the request withUNSUPPORTED_FEATURE— fires would be unverifiable. - A later seller-side change to the accepted intersection fires
principal.changedwith reasondeclarations_intersection_changedand does not advanceconfiguration_version. - The readback persists the full negotiation record: the declared set, the accepted intersection, the
selected_async_adcp_versionthe seller will actually use for pushes, and anexclusionsentry with a reason for every declared value not accepted — so “what can this buyer implementation consume, and what did the seller decide?” always has one durable answer. - Submitting
{}clears the declared set.
Reusable reporting destinations
A destination is registered once on the caller’s principal record and can later be referenced from several advertiser accounts for which that caller is independently authorized. In the example below, the authenticated principal might be a delegated human user in Claude or an autonomous buyer-agent workload; the request body is identical because identity never comes from the payload.destination_ref and one of these states:
destination_ref is not a bearer capability. It identifies one immutable destination generation for the same authenticated principal. Exact replays preserve the reference; changing provider, transport, normalized coordinates or recipient, accepted formats, access mode, or accepted verification profiles requires fresh proof and produces a new reference. Superseded references stay resolvable for existing account bindings and retained reporting history, and remain enumerable to their owner through prior_destination_refs on get_principal. Every use still checks account authorization separately, and unknown, cross-principal, and unauthorized references fail indistinguishably.
Destination lifecycle
The typical flow for a destination that needs provider-native setup:- Register. The caller syncs the destination. The seller validates it against its advertised offering, issues
destination_ref, and returnsstate: "action_required"with a typedsetupobject — for example{"action": "grant_access"}for a BigQuery dataset the seller’s published principal cannot yet reach, or{"action": "accept_share"}for a dataset share awaiting recipient activation. - Complete provider-native setup. The caller performs the action in the provider’s control plane: grants the seller’s principal on the bucket or dataset, accepts the share as the named recipient, or writes the seller-named proof object. Credentials never transit AdCP.
- Poll the read task. The caller polls
get_principaluntil the destination reachesready. Reading never mutates state or the version. Resubmitting the identical section under a freshidempotency_keyis also safe — it returnsaction: "unchanged"and preservesdestination_ref— but the read task is the intended poll mechanism. - Bind. Account-scoped reporting configuration references the
readydestination bydestination_ref, subject to its own authorization.
ready asserts is pattern-specific:
file_transfer— the caller proved control of the normalized location through a seller-verifiable challenge, and the seller successfully wrote and read back a probe object under the prefix. A grant that exists but was never exercised is notready.warehouse_materialization— the seller verified, through access it was actually granted, that it can create and commit into the locator.dataset_share— the named recipient affirmatively completed the provider’s acceptance or activation step and the provider reports the recipient able to read the share. Access modes that grant to a bare identifier with no acceptance step require an equivalent recipient-initiated proof, such as a recipient read of a seller-published proof dataset. Grant creation alone never producesready, and because provider grant propagation can lag, account-level delivery readiness additionally requires consumer-read verification as defined by managed reporting delivery.
Suspension and revocation
Destination deactivation is a delivery control, not just a registration flag:- Suspend — resubmit the destination with
active: false. The seller stops initiating new deliveries to every generation of thatdestination_idwithin its advertisedsuspension_interval_secondsand marks affected account bindings with a typeddestination_suspendedissue. Resubmitting withactive: truelifts the suspension without new proof when nothing else changed. - Revoke — omit the destination from the submitted
reporting_destinationsarray (or submit[]to revoke all). Revocation has every suspension effect, removes the destination from current state, and retains its generations underretired_destinationsfor audit and reporting history. Reusing a retireddestination_idrequires fresh registration and proof and produces a new generation.
Account consolidation
A consolidated destination can receive reporting from many seller accounts. The destination registration proves where this caller may receive data; it does not prove which data the caller may receive. The managed reporting delivery specification (RFC #6911) supplies that second authorization by binding an account-scoped feed configuration to the reusabledestination_ref. Consolidated files, rows, and partitions must retain seller and account identity so downstream consumers can reconcile them independently.
An agent principal serving many operators may set the optional operator_id label on each destination — its own bookkeeping about which operator a destination serves. That enables per-operator isolation (“this operator’s data goes only to their bucket”), audit (“list every destination serving this operator”), and one-call per-operator suspension or revocation when an operator offboards. The label confers no identity: matching operator_id values or domains across different principals are never linked, and each agent maps its operators its own way.
Reporting subscriptions
Experimental in 3.3 under
media_buy.reporting_subscriptions (RFC #8543). Use this section only when supported_sections includes reporting_subscriptions and media_buy.reporting_delivery.configuration_tasks includes sync_principal.reporting_delivery_configs on sync_accounts report nothing for a new account until someone configures it. A reporting subscription is registered once on the principal and follows the caller’s account authority, the way a caller-level webhook with all_authorized_accounts: true does. The seller turns each subscription into per-account obligations, and buyers read them through get_reporting_status as before.
scope is exactly one of all_authorized_accounts: true, an explicit acknowledgment that is never implicit, or account_ids[]. exclude_account_ids[] opts accounts out of an all-accounts subscription. operator_domains[] keeps only accounts whose operator is listed. Use it whenever the destination belongs to one operator: an all-accounts subscription otherwise follows every account the principal is granted, including accounts a different operator grants later, and would send their data to that operator’s destination. Registration requires no account authority and grants none.
An account is covered while the scope selects it and the principal is authorized for it, meaning its authorization object, when the seller advertises one, lists get_reporting_status in allowed_tasks. Within a covered account the subscription covers every confirmed media buy the principal can see through get_media_buys, including buys created later. Buys created in the seller’s UI or by other agents that the principal can’t see are not covered. The seller re-checks authority at every delivery, including retries.
“Subscribe once” means once per principal. On a seller where each operator or advertiser grants its own credential (require_operator_auth: true), each credential is a separate principal and registers its own subscriptions.
Selector. Offering IDs are per seller and per product, and an all-accounts subscription can’t name them for accounts that don’t exist yet. The subscription selects by attributes that are stable across sellers:
For example, if a seller offers
package, package × creative, and package × country breakdowns, dimensions: ["package"] resolves to the package offering under the default exact; finest would pick one of the two finer offerings by offering_id, and all would pick all three.
reconciliation_mode must also match the offering. Without a destination the subscription selects only API-delivered (Core) offerings; with one, only offerings whose method matches that destination’s pattern and transport. Billing subscriptions require official finality and consumer_receipt, and consumer_receipt requires a destination. The seller resolves the selector separately for each account and each buy’s products. Each (buy, resolved offering) pair is owed its own periods, and each obligation names its offering_id and report_definition_id. An account or buy with no matching offering gets an offering_unsupported exception. When only some of a buy’s packages resolve, the buy is covered for those, and the exception names the uncovered package_ids. Neither case fails the request or the subscription.
Period boundaries. The resolved offering’s published schedule governs period boundaries: its alignment (account timezone, UTC, an upstream source timezone, or a billing cycle), anchor, and timezone, which the buyer reads from the offering. Use the selector’s alignment to require one. period_timezone overrides the timezone for a subscription scoped to exactly one account. When an account’s timezone changes, for example through a request_account change, account-timezone periods switch at the first old-timezone boundary at or after the change. The switch is recorded in get_reporting_status as a timezone_cutovers entry, and list_accounts shows the new timezone and revision.
Destinations. destination.destination_id names an entry in this principal’s reporting_destinations, which must still exist after the same request applies. Every delivered row carries account_id. A path_template is a relative path with no . or .. segments; allowed placeholders are {account_id}, {subscription_id}, {offering_id}, and {period_start}. Each substituted value fills exactly one path segment: the seller percent-encodes (RFC 3986) any value that contains / or equals . or ... It must contain {account_id} when the scope can cover more than one account, and is then required for file_transfer destinations. When more than one active subscription of the principal shares a destination, every template must contain {account_id} and the templates must differ. When an agency must keep advertisers apart, an account-scoped subscription points that advertiser’s feed at its own destination; sellers that support subscriptions MUST support that.
Coverage. The buyer computes coverage without asking. For each covered buy and resolved offering, the owed periods are the complete periods of the offering’s schedule that:
- start on or after the first boundary at or after the latest of the subscription version’s
accepted_at(in the readback), the buy’sconfirmed_at, and the account’sauthorization.authorized_atonlist_accounts; and - overlap the buy’s flight, from its
start_timeto its stop time. The stop time is the earliest ofend_time,cancellation.canceled_at, andcompleted_at.
scope_resolved_at. So an update_media_buy extension adds only periods not yet resolved, and reinstating or extending a buy never makes an elapsed period owed after the fact. The period containing the stop time is owed in full. Timing (expected_at) comes from each obligation’s offering. The seller reports only exceptions, and each removes periods; none adds one. See get_reporting_status.
Versions and cutovers. A new subscription_version is required whenever delivery semantics or scope change: the selector, reconciliation_mode, destination, scope, exclude_account_ids, or operator_domains. Each version has its own accepted_at. active and replace_account_configs change in place. Changes are cutovers at the instant they take effect, T. The outgoing schedule owes its periods that start before T, in full, ending at E. The incoming schedule owes its periods that start at or after E. No period is owed twice. When the two schedules’ boundaries don’t align, the stretch from E to the incoming schedule’s next boundary is owed under neither; it is shorter than one incoming period. Version changes (for accounts in both versions, T is the new accepted_at), deactivation, overrides, replace_account_configs, offering resolution changes, and account timezone changes all use this rule. See cutovers.
Obligations from a subscription carry delivery_config_id equal to subscription_id and delivery_config_version equal to subscription_version. Subscription IDs and the principal’s sync_accounts delivery_config_id values therefore share one namespace, and the seller rejects a collision. A subscription_id is never . or ...
The section is complete desired state like the others. Omitting a subscription, or setting active: false, deactivates it as a cutover at that moment: periods already started stay owed in full and no later ones are created. Deactivated subscriptions stay readable in get_principal as inactive while their obligations are retained. The seller rejects the whole request with INVALID_REQUEST when two active subscriptions at the same scope level would both apply to one account and feed purpose.
Precedence. Precedence is per account and feed_purpose, and only among the principal’s own registrations. During 3.x the principal’s own active sync_accounts reporting_delivery_configs entry for the account replaces every one of its subscriptions for that account and purpose, whether all-accounts or account_ids. Otherwise its account_ids subscription naming the account replaces its all-accounts subscription. Another principal’s configurations and subscriptions never affect this principal’s subscriptions, and overridden_by never names them. A replaced subscription records an overridden exception naming the overriding delivery_config_id or subscription_id, so a buyer never gets two feeds and two sets of obligations for one account and purpose.
To end its own account-level overrides without calling sync_accounts, a principal sets replace_account_configs: true on the subscription. The seller applies it once per subscription_version, or when the flag changes to true, not on every resend. It deactivates the principal’s own active reporting_delivery_configs with the same feed_purpose, only on in-scope accounts where the subscription resolves an offering. It never touches accounts the subscription excludes, or with offering_unsupported or capacity. Each configuration is cut over to the subscription: the configuration keeps the periods it started, and the subscription owes its periods from the end of the last of them. sync_accounts and list_accounts readback show the configuration inactive, and the overridden exception ends at the cutover. A later sync_accounts entry takes precedence again. Subscription writes, including these deactivations, are caller configuration, not account changes: they never advance the account’s revision, so they never conflict with an in-review request_account change.
Seller limits. A seller may declare media_buy.reporting_delivery.subscriptions.max_accounts. Beyond it, or for an account it won’t serve, the seller records an excluded exception with reason capacity or action_required (with a pending action), rather than being forced to serve every account. The served set is deterministic and reported, never silently partial.
Exceptions without polling. get_principal lists, per subscription, accounts_with_exceptions[]: each account with an open exception and its reasons. An agency doesn’t have to poll get_reporting_status account by account. principal.changed with reason reporting_subscription_changed fires whenever an account’s exception opens or closes.
Lost authority. When the principal loses authority for an account the subscription covered, get_principal lists it under that subscription’s accounts_unauthorized[] with unauthorized_at, last_owed_period_end, and the count of withheld periods. The entry stays until re-grant or for status_retention_days. After that it drops, and a later re-grant is a fresh coverage start from the new authorized_at, with no gap claimed. Each addition or removal fires principal.changed with reason reporting_subscription_changed, as does a subscription moving to action_required. get_reporting_status treats the account as unknown, as for any unauthorized caller.
How coverage starts and ends, and what happens on revocation and re-grant, is in get_reporting_status.
Verification profiles
Destination setup advertises verification profiles the caller can accept:native_commit: provider-native transaction/version evidence with row counts and control totals. Appropriate for frequent warehouse snapshots when allowed by the feed profile.manifest_checksums: a manifest-last file commit with cryptographic object checksums.canonical_digest: recomputation of a canonical logical-content digest. Appropriate for billing and strict-conformance feeds.
Secrets and proof
AdCP carries destination coordinates and recipient identifiers, never provider credentials. Private keys, passwords, bearer profiles, signed URLs, and cloud credentials are exchanged through provider-native control planes or an out-of-band onboarding channel. The schema pattern onlocation is a transport guard, not the enforcement boundary: sellers additionally screen canonicalized values for credential shapes the pattern cannot express, such as userinfo-style key:secret@host prefixes inside an otherwise valid locator.
Before proof binding and uniqueness comparison, sellers canonicalize coordinates per transport — Unicode NFC, lowercase scheme and host authority, no default ports, canonical path separators — and reject locators or recipient identities whose host authority is not ASCII after canonicalization. Proof binds to the canonical form, so cosmetic variations of the same coordinates cannot evade the fresh-proof requirement.
Before a destination becomes ready, the seller completes the pattern-specific proof and binds it to the authenticated principal, seller agent, destination ID, normalized coordinates, and accepted delivery contract. A changed proof-bound tuple requires fresh proof and a new immutable destination reference; replacement never mutates the meaning of a reference already used by an account configuration.
For notification_configs, the endpoint proof and webhook-signing rules are identical to sync_agent_notification_configs. Implementations exposing both tasks maintain one underlying caller-scoped subscriber set.
Errors
Failed responses carryresult.kind: "failed" with structured errors and deliberately cannot carry a principal ID, version, or configuration state.
Provider-side setup failures for in-offering destinations are not request errors: they surface as
validating, action_required, or rejected destination states inside an applied response.
Compatibility
sync_agent_notification_configs remains the specialized compatibility task for agents that only support capability-change subscribers. When capability_changes.notifications.registration_task is sync_principal, clients manage those subscribers through the notification_configs section here.
The broader task does not turn get_adcp_capabilities into caller-specific state. Capabilities remain cacheable seller facts; principal configuration remains authenticated caller state.