Skip to main content
The Accounts Protocol defines the commercial layer beneath all AdCP vendor protocols. Every transaction — a media buy, a data signal, a content standards check — happens between parties that have a commercial relationship. The Accounts Protocol establishes that relationship and provides consumption reporting so vendors can track how their services were used.

The commercial model

Seven questions underlie every AdCP transaction: Every account-scoped task names an account. The seller’s get_adcp_capabilities response tells the buyer how to get one, and require_operator_auth is the field that decides it. Read it as who holds the credential the seller authorizes: The operator in a natural key is a property of the account: the agency or brand the account is for, and whom the seller checks against brand.json and may invoice. It is not an authenticated party. With require_operator_auth: false, no operator authenticates; the agent does, and names the operator in the payload. require_operator_auth does not by itself declare whether OAuth is used, whether list_accounts is exposed, or which sync_accounts modes are supported. When require_operator_auth is true (account-id namespaces), operators authenticate independently and buyers pass seller-assigned account_id values because the seller or upstream platform owns the canonical account namespace. If a credential may access more than one account, the seller MUST expose list_accounts and buyers MUST resolve an explicit account_id before the first account-scoped request. If a credential is bound to exactly one account, the seller SHOULD expose list_accounts returning that singleton, and MAY omit it only when the same explicit account_id is supplied through another declared path or out-of-band onboarding. When require_operator_auth is false (buyer-declared accounts), the agent is trusted. The buyer opens each account with brand + operator and sends the returned account_id on later calls. Most sellers provision these relationships through sync_accounts. A seller MAY instead lazily provision on the first provisioning task, such as create_media_buy, when billing and every other required setting are unambiguous from capabilities or the authenticated agent’s onboarding defaults. Discovery tasks never provision; see Account references before provisioning. A lazy-provisioning seller MUST keep accepting the natural key and MUST expose list_accounts for recovery. If buyer input is required before use, the seller MUST expose sync_accounts. Follow Provision a seller-mediated account for a complete buyer-declared workflow from discovery through human setup, activation, and first spend. Follow Set up your buyer agent with a seller for the standing, caller-scoped setup that comes before accounts: webhooks, reusable reporting destinations, and declarations. An ad network may use both models simultaneously — buyer-declared accounts on the buyer-facing side (the network is agent-trusted) and an account-id namespace with each underlying platform (the network authenticates as an operator). See the Sponsored Intelligence guide — account model for networks for the full account chain: buyer agent → network (buyer-declared) → AI platform (account-id namespace). After delivery, the orchestrator calls report_usage to inform vendor agents (signals, governance, creative) how their services were consumed. This is not settlement — it’s consumption reporting so the vendor can track earned revenue and verify billing.

Scope

The Accounts Protocol applies across all vendor protocols. An orchestrator establishes an account once per brand/operator pair per vendor agent and reuses the same account reference across all interactions with that agent: The account reference is the seller-assigned account_id in both models. In this vocabulary, brand identity is the brand’s domain, plus brand_id where its brand.json lists several brands. Operator identity is the operator’s domain, and account identity is the account_id. brand.countries, operator_unit, currency, timezone, and sandbox are account attributes, not identity. operator_unit.id is deliberately separate from the seller’s account_id. During 3.x, a buyer may still send the natural key for a buyer-declared account: brand + operator, with attributes as filters. It resolves only when exactly one account matches. See Account identity and Account references for details. billing names the party the seller invoices for this account relationship. It is not a payment rail or settlement selector. AdCP does not currently standardize per-media-buy choice between an intermediary clearing flow and direct settlement; implementations must not encode that distinction by silently changing the meaning of billing.

Account references before provisioning

A buyer-declared natural key finds an advertiser account. It does not create one. An account is provisioned once sync_accounts has returned it without action: "failed", or once a lazy-provisioning seller has created it on a provisioning task (below). Before that, the key does not resolve. After that, account status decides which tasks the account can use. Only provisioning tasks provision. A lazy-provisioning seller provisions only on a task that commits spend or creates account-owned resources, such as create_media_buy, buy_products, accept_proposal, sync_creatives, sync_catalogs, sync_event_sources, or activate_signal. Discovery and negotiation tasks, including get_products, list_products, get_signals, request_proposals, refine_proposals, and decline_proposals, MUST NOT create or activate an account or accept the seller’s default terms on its behalf. An unresolved account is an error, not a fallback. When a request carries an account that does not resolve, the seller MUST return ACCOUNT_NOT_FOUND, including on tasks where account is optional. It MUST NOT drop the reference and answer with public results: cache_scope: "public" on an account request says that this account prices off the rate card, which is a claim about an account that does not exist. One exception: a seller that lazily provisions instead of exposing sync_accounts MAY answer a discovery task for a natural key it would provision on the next provisioning task. It answers as if that account existed with the settings it would get, cache_scope follows the usual rule for that account, and the account is not created. list_accounts is different: its account field is a filter, so a key that matches nothing returns an empty list. Buyers omit unprovisioned keys. Until an account is provisioned and its status allows the task, buyers SHOULD omit account on discovery tasks and send top-level brand where the task accepts it. The seller answers with its public view (cache_scope: "public"). When the seller declares account.required_for_products: true, the buyer provisions the account before calling get_products. A buyer that intends to accept a proposal provisions the account before request_proposals and sends it there, so the proposal is bound to that account. Buyer SDKs SHOULD store the account_id of each account the buyer opens and apply these rules by default, so each implementer doesn’t have to. Account errors describe setup, not seller health. ACCOUNT_NOT_FOUND, ACCOUNT_SETUP_REQUIRED, and ACCOUNT_PAYMENT_REQUIRED describe the relationship between one buyer and one seller. Buyers SHOULD NOT count them toward a seller’s availability or health. To recover from ACCOUNT_NOT_FOUND on a natural key, provision through sync_accounts (or omit account for public discovery). For an account_id, verify it through list_accounts. After opening, references use the account ID. Sellers MUST return account_id from sync_accounts once the account is resolved, accept it on every account-scoped task, and never change it. Buyers store it and send { "account_id": "..." }. The seller keeps accepting a natural-key reference throughout 3.x. See Account identity.

Account identity

Every party to an account is identified by a domain, verified by what that domain publishes: An account is identified by the account_id the seller issues when the account opens. Nothing else identifies it:
  • Open. A sync_accounts entry without account opens an account for a brand and an operator, and the seller returns its account_id once the account is resolved. A seller never issues an account_id it might later change, so a pending_approval row that may still resolve to a different account carries an experimental request_id instead. A retry with the same idempotency_key returns the original result. When an entry matches exactly one account in the seller’s account scope, the seller SHOULD return that account_id instead of opening another. The match follows the request_account deduplication guidance: brand and operator, with the attributes as filters and billing terms never filtering. The exact-or-reject rules for billing and payment_terms still apply. An entry whose attributes differ, such as its countries, opens a second account with its own account_id. A provisioning entry never changes an existing account. A lazy-provisioning seller opens the account on the first provisioning task instead.
  • Reference. After the account opens, buyers send { "account_id": "..." }. A natural-key reference (brand + operator) resolves only when exactly one of the caller’s accounts matches it. Each supplied attribute must equal the account’s value: countries compares as a set, and operator_unit compares by id. An omitted attribute does not filter, except sandbox, which defaults to false. When several accounts match, the seller returns ACCOUNT_AMBIGUOUS and MAY list them in error.details.candidates.
  • Update. brand.countries, operator_unit, currency, timezone, and sandbox are account attributes. The buyer sets them when the account opens, and a sync_accounts entry with account updates them, as it updates billing. The seller validates a changed attribute as it does when the account opens. The account_id stays the same. A seller that does not support changing an attribute rejects the entry with UNSUPPORTED_FEATURE. currency, timezone, and sandbox are set at opening: a seller MAY refuse a currency or timezone change, changing one normally means a new account, and sandbox never changes. A seller can review a countries change first, and the entry’s result then reports pending_approval while the account keeps its current countries; see Updating account attributes.
Because an account’s identity never changes, its account_id keeps naming it. No former key exists to tombstone or redirect, and no update can collide with another account’s identity. Sellers that review requests by hand, or may link a request to an existing account, can offer request_account (experimental in 3.3). A request is its own object, so no account_id exists until it resolves, a match never changes an ID, and pending_action.waiting_on says whose turn it is.
Deprecated in 3.3: the natural key as the account reference after opening. Sending the natural-key AccountRef on account-scoped tasks once an account exists is deprecated and is planned for removal in AdCP 4.0 (#7850), under the standard deprecation policy. It keeps working throughout 3.x, and the natural key stays valid as a list_accounts filter.Deprecated in 3.3, planned for removal in 4.0: the identity-rekey machinery. The account.identity_updates capability, operator_identity reconciliation through sync_accounts, and the account.stable_account_id capability are deprecated. So are the ACCOUNT_MOVED and ACCOUNT_IDENTITY_CONFLICT error codes. Sellers that implement operator_identity keep it working through 3.x. stable_account_id always holds, so buyers send account_id without checking it.

Account Status Lifecycle

Accounts progress through a defined set of states. Terminal states (rejected, closed) allow no further transitions.
Transition rules:
  • pending_approval → active: seller approves after credit/contract/identity review
  • pending_approval → rejected: seller declines. Terminal — buyer must submit a new account request.
  • active → payment_required: automatic when credit limit is reached or funds are depleted
  • payment_required → active: when the buyer resolves the outstanding balance. Sellers MAY auto-transition or MAY require manual re-activation.
  • active → suspended: seller-initiated (policy violation, billing dispute, fraud review). Sellers MUST notify orchestrators via webhook.
  • suspended → active: seller-initiated reactivation
  • suspended → closed: seller-initiated permanent closure
  • active → closed: seller or buyer-initiated permanent closure. Terminal.
  • Sellers MUST reject operations on accounts in terminal states with ACCOUNT_NOT_FOUND or an appropriate error

Operations by Account Status

Account status acts as a gate on which tasks are permitted, as shown below. Mutation operations are the most restricted.
  • payment_required blocks new spend (create_media_buy) but allows managing existing buys and resolving setup. Sellers SHOULD also reject new_packages within update_media_buy when the account is in payment_required, since adding packages is functionally equivalent to new spend.
  • suspended allows read-only access to existing data but blocks all mutations
  • Sellers MUST return ACCOUNT_SUSPENDED for blocked operations on suspended accounts and ACCOUNT_PAYMENT_REQUIRED for blocked operations on payment-required accounts

Caller authorization

Not every caller with access to an account has the same grant. A vendor agent may issue one calling agent a full scope and another agent a narrow read-plus-update scope. Authentication confirms the caller is who they claim to be; authorization answers “what is this caller allowed to do on this account?”
Applies to every vendor protocol. The authorization mechanism described here is part of the shared Accounts Protocol — it applies to every agent that implements sync_accounts / list_accounts: media-buy sales agents, signals agents, governance agents, creative agents, brand agents. Signals agents scope activation vs. catalog access; governance agents scope audit read vs. plan management; creative agents scope library read vs. upload. Only the standard named scope attestation_verifier is Media Buy Protocol-specific (it binds to the AAO Verified (Live) qualifier); the rest of the machinery — allowed_tasks, field_scopes, read_only, custom:-prefixed scopes — is protocol-neutral.
Schema: /schemas/3.3.0-beta.1/core/account-authorization.json Vendor agents that support scope introspection attach an authorization object to each per-account entry in sync_accounts and list_accounts responses:

Fields

AdCP 3.2 compact product tools. Authorization names the actual task. A get_products grant does not silently authorize request_proposals, refine_proposals, or decline_proposals; sellers grant the proposal lifecycle explicitly and may scope its compact top-level fields independently. A read-only grant may permit list_products but rejects the three mutation-capable proposal tools.

Semantics of presence and absence

  • Present: the vendor agent asserts the shape reflects its enforcement for this caller on this account at this moment. Stale-by-seconds is fine; systematically-divergent is non-conformant.
  • Absent on a single account: the vendor agent is not telling the caller their scope for that account. Caller falls back to error-driven discovery (try the task, handle the RBAC error codes).
  • Absent on all accounts: the vendor agent does not implement scope introspection. Callers MUST NOT infer access from absence — the vendor agent may still enforce scope locally and return SCOPE_INSUFFICIENT / READ_ONLY_SCOPE / FIELD_NOT_PERMITTED on any task call.
  • authorization is optional for 3.x. Vendor agents that want to avoid a breaking change can defer populating it. Populating is strictly additive — it lets callers preempt errors they would otherwise discover by trying.
Vendor agents claiming the attestation_verifier standard scope (Media Buy Protocol-specific) MUST populate authorization — the AAO Verified (Live) attestation flow depends on verifying the advertised scope matches enforcement.

Identity binding, refresh cadence, and consistency

The authorization object is implicitly scoped to the (caller identity, account_id) tuple at read time. The same account returned to a different authenticated caller MAY return a different authorization object — that’s the point of the RBAC model. Vendor agents MUST resolve caller identity from the authenticated request (not from any client-supplied field) and MUST bind the returned authorization to that resolved identity. Refresh cadence.
  • Callers SHOULD re-read authorization at least every 300 seconds of active use against an account, via sync_accounts or list_accounts (filtered by the held account).
  • Vendor agents MUST reflect operator-initiated scope changes in sync_accounts / list_accounts responses within 300 seconds of the change being made. A compliant caller polling every 300s sees operator changes within at most one refresh cycle.
  • Vendor agents MAY cache the authorization object briefly but MUST NOT cache past 300 seconds without revalidation.
  • The 300s figure is a floor, not a target — vendor agents whose scope changes more frequently SHOULD surface them faster, and callers running AAO Verified (Live) attestation SHOULD probe at the check-7 cadence (at least once per rolling window, plus on every observed scope change).
Consistency. For a given (caller identity, account_id) tuple, sequential reads within the refresh window MUST return identical authorization objects, modulo operator-initiated scope changes. Flicker from load-balanced or eventually-consistent backends — two reads 10 seconds apart returning different allowed_tasks because they hit different replicas — is non-conformant. Compliance engines and coding agents rely on scope stability for state tracking; a vendor agent that cannot guarantee it MUST omit authorization rather than populate it inconsistently. This is the concrete form of the “systematically-divergent is non-conformant” guarantee from the presence semantics above — it’s verifiable: a conformance check can read the same account twice from the same identity inside the refresh window and diff the results.

Buyer response to SCOPE_INSUFFICIENT within the refresh window

A single SCOPE_INSUFFICIENT response is observationally indistinguishable between two causes: a seller replica that has not yet propagated a valid grant (transient infrastructure artifact — will resolve), and a legitimate scope reduction by the operator (persistent — must surface). Before classifying the error as a definitive correctable signal requiring operator intervention, buyers MAY exhaust a small bounded retry budget to disambiguate the two:
  • Retry budget: no more than 3 attempts, each separated by 1–5 seconds of jittered backoff. This is disambiguation logic — establishing whether the scope is genuinely insufficient before trusting the correctable classification — not a recovery action for a correctable error.
  • Not the 300s window: the retry budget is not the seller’s propagation SLA. Buyers SHOULD NOT wait out the full 300-second window on every false negative; up to 15 seconds of accumulated backoff delay is sufficient to absorb typical replica lag.
  • After retries exhaust: buyers MUST surface the error. “Surface” means: return a structured error to the calling layer (including account ID, the failed task, and error.details.retry_count with the number of attempts made) AND ensure the failure is visible in an operator-accessible channel — a structured log, dashboard alert, or notification. The escalation mechanism is implementation-defined; the requirement is that the error is not silently swallowed.
READ_ONLY_SCOPE follows the same bounded-retry logic when the buyer suspects grant-propagation lag (a replica lagging on a write-grant update). Caution: if write access was recently revoked, a stale replica may accept a mutation that the revoked scope should have rejected. If bounded retries succeed, buyers MUST re-read authorization before treating the result as reliable — and if re-reading confirms that write access has been revoked, buyers MUST surface an alert to the operator flagging the mutation as potentially unauthorized; they MUST NOT silently accept it as correct. FIELD_NOT_PERMITTED does not follow this pattern. The agent-autonomous recovery path — strip the disallowed field and resubmit — supersedes the retry consideration. Buyers MUST NOT retry the identical failing request for FIELD_NOT_PERMITTED; they SHOULD correct and resubmit immediately. See Authorization (RBAC) for the normative retry exception clause on these codes.

Standard named scope: attestation_verifier

Media-buy sales agents advertising AAO Verified (Live) readiness (tracked in #2965) MUST support a named scope identified by scope_name: "attestation_verifier" with the following minimum shape:
  • allowed_tasks (at minimum — vendor agents MAY include additional read-only tasks):
    • get_adcp_capabilities
    • get_products
    • get_media_buys
    • get_media_buy_delivery
    • list_creatives
    • update_media_buy
  • field_scopes.update_media_buy: ["reporting_webhook"]
  • read_only: false
The scope deliberately omits create_media_buy, sync_creatives, and all spend-committing or targeting-modifying fields on update_media_buy. It is narrowly designed for continuous-observability verification — the compliance engine can discover live campaigns, read inventory and creative state, attach a verification reporting webhook, and read delivery, but cannot book inventory, modify budgets, change flight dates, upload creatives, or cancel anything. get_products and list_creatives are included because AAO Verified (Live) observability requires sanity-reading the seller’s declared inventory and the creative pipeline state on active buys. attestation_verifier is Media Buy Protocol-specific — it binds to the AAO Verified (Live) qualifier, which is a media-buy flow (live observation requires real ad delivery). Equivalent observability scopes for signals, governance, creative, or brand agents are not yet standardized; until they are, those agents use custom: scopes.
Reporting only — lifecycle-running is a future scope. attestation_verifier is the reporting half of (Live): the engine observes campaigns the seller has trafficked, reads delivery, and attaches a verification webhook. It deliberately cannot create campaigns, attach creatives, or modify budgets. The complementary lifecycle-running role — for the AAO-operated canonical-campaign runner contemplated in #3046, where the engine traffics canonical PSAs end-to-end through a seller’s live agent — needs a broader write scope (attestation_runner, tracked in #3561). Today’s brownfield enrollment (Path B) requires only attestation_verifier; the runner-side scope lights up alongside the canonical-campaign runner itself.

Custom scopes for other vendor protocols

Any vendor agent MAY define custom scopes using the custom: prefix. Buyers MUST NOT assume any semantics from a custom-prefixed scope name — the names are agent-defined and learned out-of-band (docs, onboarding). Illustrative examples (not standardized — each agent names its own):
  • Signals agent: custom:activation_only — allowed_tasks: [get_signals, activate_signal, get_adcp_capabilities], no catalog management, no cross-account metadata.
  • Governance agent: custom:audit_viewer — allowed_tasks: [get_plan_audit_logs, get_adcp_capabilities], read_only: true. Useful for regulators or external auditors granted read access to governance trails.
  • Creative agent: custom:library_reader — allowed_tasks: [get_adcp_capabilities, list_creatives], read_only: true. Lets a non-uploading buyer (e.g., a measurement partner) discover capabilities and library contents without mutating them.
  • Measurement agent on a buyer-orchestrator gateway account: custom:measurement_feedback — allowed_tasks: [get_media_buy_delivery, provide_performance_feedback], read_only: false. These are the fixed tasks in the first experimental gateway tier. Measurement providers do not receive seller-account grants. The exact task list is normative, so no standard named scope is needed.
  • Brand agent (rights): custom:rights_viewer — allowed_tasks: [get_rights, get_brand_identity], read_only: true. Discovery without clearance privilege.
The presence/absence semantics, identity binding, refresh cadence, and consistency requirements above apply uniformly across all vendor protocols — a signals agent populating custom:activation_only takes on the same 300s refresh obligation a media-buy seller does populating attestation_verifier.

Prior art

The introspection model — “the caller asks the authorization-enforcing party what the grant is” — is structurally analogous to RFC 7662 OAuth 2.0 Token Introspection, specialized for AdCP’s task-and-field authorization model. Embedding the response in sync/list rather than splitting it into a separate task reflects that account discovery and scope introspection are the same natural question (“what are my accounts, and what can I do with them?”) — the two are returned together.

Transaction lifecycle

Parties

The Accounts Protocol operates with four party types. See Accounts and agents for full details on billing hierarchy, trust models, and authorized operators.

Tasks

Account discovery (normative). Every agent accepting accounts MUST expose at least one of list_accounts or sync_accounts. A seller that supports the experimental account.requests surface also exposes request_account (open or change an account) and list_accounts (read accounts and requests); buyers SHOULD use those with it, and sync_accounts remains available for buyers and sellers that don’t. A seller-defined account-id namespace MAY omit both only when account IDs are supplied out-of-band and no account settings are managed through AdCP. Buyer-declared-account sellers normally expose sync_accounts to establish the relationship and SHOULD also expose list_accounts for cold-start recovery. A stateless buyer that lost its account IDs cannot recover them by replaying sync_accounts, which opens accounts. list_accounts therefore returns each account’s account_id with its brand, operator, and attributes. A buyer-declared seller MAY omit sync_accounts only when it lazily provisions from a natural key, can resolve every required setting without buyer input, and exposes list_accounts. For account-id namespaces, sync_accounts is settings-update only in 3.0.x unless a future explicit capability declares account-id provisioning. See Required tasks by protocol.

Brand registry connection

The brand.domain in account references is not an arbitrary identifier — it is the brand’s domain, resolvable to a brand.json file that declares the brand’s canonical identity, sub-brands, authorized operators, and properties. Vendor agents can verify buyer claims against the brand registry: if an orchestrator claims to represent acme-corp.com, the vendor can fetch acme-corp.com/.well-known/brand.json to confirm authorized operators and brand hierarchy. This makes the Accounts Protocol tamper-resistant — account relationships are grounded in publicly verifiable brand identity. See the Brand Protocol for how brand identity resolution works.

Counterparty verification

Every commercial relationship in advertising depends on knowing who you’re actually doing business with. The Accounts Protocol addresses this at the protocol level through the brand registry. When an orchestrator references an account, the brand.domain identifies the advertiser. Vendor agents can fetch brand.domain/.well-known/brand.json to verify:
  • Brand identity: Is this brand who they claim to be?
  • Operator authorization: Is the operator listed in the request actually authorized to buy on this brand’s behalf?
  • Brand hierarchy: Which sub-brands does this house portfolio include?
This verification is grounded in publicly accessible DNS-hosted identity — not in what the buyer agent asserts, but in what the brand itself has declared. The pending_approval account state is where human review occurs: credit checks, legal agreements, and identity verification. Vendor agents that require these steps return a setup.url for the human to complete the process before the account becomes active.

Brand registry and the contribute-back pattern

The AgenticAdvertising.org brand registry provides a community-maintained layer of brand identity for brands that haven’t yet published their own brand.json. Buyer agents resolving brands before account setup can contribute data back to the registry as a byproduct of normal workflows — improving identity coverage for the ecosystem without extra effort. The recommended pattern for buyer agents uses three building blocks (see #1166):
confirmWithUser is a placeholder for whatever confirmation mechanism fits your UX — an explicit prompt, a review step in a workflow UI, or a low-confidence flag that triggers human review. The confirmation step is what makes the improvement loop work: enrichment data comes from third parties and isn’t guaranteed to be correct. User verification before the data is used in a live campaign is what keeps the registry accurate over time.

Source authority

The registry tracks where brand data came from. Sources in descending authority: When an agent calls save_brand or research_brand, the registry applies merge logic: existing fields from a higher-authority source are preserved, and only missing fields are filled in. This respects what brands have declared while filling gaps. research_brand skips re-enrichment if the registry already has recent enriched data for the domain, avoiding redundant API calls. The full edit history for any brand — who contributed, when, and with what summary — is queryable via GET /api/brands/history.

Property contribute-back

The same pattern applies to publisher properties. When a buyer agent discovers a new publisher through a sales agent interaction, it can contribute that property back to the registry via POST /api/properties/save. This improves property coverage for the ecosystem the same way brand contribute-back improves brand coverage. See Registry API — save property for details.

Usage reporting

Vendor agents (signals, governance, creative) are not direct participants in campaign execution — the orchestrator uses their services as inputs to a media buy. After delivery, report_usage tells these vendors what was consumed so they can track earned revenue and verify billing. report_usage is buyer-reported: the orchestrator computes and reports consumption. Each record carries its own account, operator_id, and kind ("signal", "content_standards", "creative"). The vendor agent uses the reported pricing_option_id to verify the correct rate was applied. Partial acceptance is valid — a single request can span multiple accounts, operators, and campaigns. The response confirms how many records were accepted and which (if any) failed validation.