Experimental. The principal layer (
sync_principal, get_principal, the adcp.principal capability block, and the principal.changed webhook) is an experimental AdCP 3.2 surface. It may change between 3.x releases with at least 6 weeks’ notice. Sellers implementing any part of it MUST declare protocol.principal in experimental_features. See experimental status for the full contract.Scenario
Pinnacle Agency’s buyer agent will buy from Priya’s StreamHaus sales agent. Sam, who runs Pinnacle’s buying, wants the relationship set up before any media buy: webhooks registered once, a reusable reporting bucket proven, and a way to stop delivery if a bucket is ever compromised. In the steps below, “the agent” is Pinnacle’s buyer agent acting under its own credential, and Sam is the person who reviews and operates it. Two things are deliberately not part of this flow:- There is no
register_buyer_agenttask. AdCP begins after authentication. StreamHaus’s authorization system decides who its callers are. - Nothing here grants access to an advertiser account. The principal layer says where and how the principal receives data. Which advertisers it may act for is established separately through
sync_accounts.
1. Get credentials from the seller
Credentials come from StreamHaus, out of band from AdCP: an OAuth client for the agent’s workload identity, an API key issued to the agent, or a verified signed-request identity published in Pinnacle’sbrand.json. The agent then authenticates as itself on every call, and StreamHaus resolves that credential to one durable principal record with principal_kind: buyer_agent.
Which credential shape StreamHaus offers is its authorization design. The protocol requires only that the credential map to a stable principal: rotating a key or renewing a token preserves the configuration only when StreamHaus maps the new credential to the same principal, and a raw key ID, token, or session is never the owner. For a signed caller, the stable subject is the canonical brand.json agents[].url, so two agent URLs under Pinnacle are two principals.
A person using an interactive client follows the same lifecycle with a different credential:
- Sam, working in Claude or ChatGPT against StreamHaus, authenticates as Sam: their own login at the seller, resolved as
principal_kind: operator. - The client application is the instrument, never the owner. ChatGPT and Claude are not the buyer agent, and nothing registers them.
- If a client can present only one shared service credential for all its users, StreamHaus must refuse to persist configuration under it until the client completes delegated authorization or account linking.
operator principal is invisible to Pinnacle’s agent, and Sam’s own suspension controls affect only Sam’s configuration. Decide which principal owns each destination before registering it. Linking two principals needs an explicit seller-side administrative operation.
Never send identity in a request body. sync_principal and get_principal reject buyer_agent_url, agent_url, principal_id, and connection_id, and a seller never derives identity from them.
2. Discover what the seller supports
The agent callsget_adcp_capabilities and reads the adcp.principal block together with the seller’s reporting offerings:
supported_sectionslists the sections StreamHaus accepts. An unsupported section is rejected, not ignored.reporting_destination_offeringsis StreamHaus’s objective offering. The agent picks a pattern, transport, format, and verification profile from it before submitting, so an out-of-offering destination is never discovered through a rejection. The account-level reporting offerings used in step 5 are advertised separately undermedia_buy.reporting_delivery.suspension_interval_secondsis the longest StreamHaus may take, after a suspension or deactivation, to stop new deliveries and webhook fires.
3. Verify who StreamHaus thinks you are
Before changing anything, the agent callsget_principal. It carries no parameters and has no side effects; it never creates a principal record or issues an identifier.
recognized means the credential maps to a durable principal that already exists and has no standing configuration. principal_kind is what StreamHaus resolved, not what the caller claimed; to catch a credential mix-up early, send expected_principal_kind: "buyer_agent" on a write, and a mismatch fails with CONFLICT before anything changes. A seller that does not pre-create records returns unconfigured instead, and the first sync creates the record. A failed result carries only errors, never identifiers.
4. Sync declarations, webhooks, and a reusable destination
The agent sends one request. Each section is the complete desired state for that section, omitted sections are untouched, and all sections apply together or not at all.declarationsstates what the buyer agent can consume: which async payload versions, which webhook signing algorithms it can verify, which experimental features it opts into. The seller returns the accepted intersection.notification_configsregisters a webhook subscriber. One subscriber can carry capability and principal changes, andprincipal.changedis the doorbell for seller-driven transitions such as a destination becomingready.reporting_destinationsregisters a reusable, non-secret destination. It holds coordinates only: credentials, private keys, and signed URLs never transit AdCP.
sync-principal-request.json
ready until it succeeds. To rehearse without side effects, add "dry_run": true: the seller validates and reports the would-be action (would_update, would_be_unchanged, or would_clear) without persisting, issuing identifiers, or challenging anything.
The proof flow: action_required to ready
StreamHaus validates the destination against its offering, issues a destination_ref, and returns the applied state. The destination is not usable yet, so its state is action_required and the response names the one typed action to take:
Schema: sync-principal-response.json
- Do the typed action. AdCP carries only the action name (
prove_control,grant_access,accept_share, orcontact_support), an optionalsetup_url, and an optional expiry. The details, such as which proof object to write under the bucket prefix, come from StreamHaus’s onboarding material or itssetup_url. Treatsetup_urlas an untrusted link, surface it to Sam, and never fetch or execute it. Credentials stay in the provider’s own control plane. - Wait for the doorbell, or poll. StreamHaus fires
principal.changedwhen the destination moves. The agent can also callget_principaluntil the state reachesready; ifsetup.expires_atpasses first, resubmit the destination to obtain a fresh action. Reading never advancesconfiguration_version, and neither does the seller’s own transition, so a read-then-write loop stays safe. - Treat
readyas the gate. Only areadydestination may be bound to an account feed.rejectedmeans StreamHaus cannot use those coordinates and the request needs a different destination.
destination_ref and requires fresh proof. The old reference stays resolvable for existing bindings and reporting history and is listed under prior_destination_refs. Existing feed bindings do not move on their own: to redirect delivery, bind a new delivery_config_version to the new reference once it is ready. Resubmitting the identical destination is a no-op: action: "unchanged" and the same reference.
The declarations section returns the negotiated contract. If the agent had declared a version or algorithm StreamHaus does not support, it would be absent from accepted and listed under exclusions with a reason, and a declared signing algorithm that shares nothing with the seller’s would fail the request with UNSUPPORTED_FEATURE while a subscriber is active.
5. Authorize accounts and bind the feed
ready proves only where the principal can receive data. It says nothing about which data. To get Nova Motors’ reporting into the bucket, the agent establishes the account relationship with sync_accounts, as in Provision a seller-mediated account, and binds the account’s feed to the reusable destination through reporting_delivery_configs. The example below is abbreviated: include timezone, payment_terms, and billing_entity as that walkthrough does, and choose offering_id and report_definition_id from the seller’s media_buy.reporting_delivery offerings.
Schema: sync-accounts-request.json
- The destination. StreamHaus resolves
destination_refonly for the principal that registered it, and a new binding requires it to beready. An unknown, cross-principal, or unauthorized reference fails indistinguishably. - The account. StreamHaus authorizes the disclosure of Nova Motors’ feed to this principal for this scope. Possessing a
readyreference is never account authority, and the account check is independent of the destination’s state.
get_reporting_status. Reliable reporting is its own experimental surface (media_buy.reporting_delivery); see experimental status.
6. Read it back, and know the kill switch
get_principal is the durable answer to “what does StreamHaus think my standing configuration is?”. The agent uses it to bootstrap expected_configuration_version before a guarded replacement, to recover after a restart, and to observe setup transitions.
Schema: get-principal-response.json
configuration_version back as expected_configuration_version on a guarded sync_principal. If two services share one principal and one is stale, StreamHaus rejects the stale write with CONFLICT and changes nothing; read again, reconcile, and resubmit under a new idempotency_key. Retries of one logical request reuse both the key and the body.
The kill switch
These controls stop delivery on the caller’s own principal. They are only as trustworthy as the credential that drives them: if a credential is compromised, revoke it in the seller’s authorization system, because anyone holding it can reverse these settings.- Pause a webhook. Resubmit the subscriber with
active: false. Fires to that endpoint, across every account, halt withinsuspension_interval_seconds. - Suspend a destination. Resubmit the destination with
active: false. New deliveries to every generation of thatdestination_idhalt within the same bound, and every bound account is marked with a typeddestination_suspendedissue. Resubmitting withactive: truelifts it without new proof when nothing else changed. - Revoke a destination. Omit it from
reporting_destinations, or send[]for all. Revocation has every suspension effect, removes the destination from current state, and keeps its generations underretired_destinationsfor audit. Reusing thedestination_idlater needs fresh registration and proof.
reporting_delivery_configs entry, or changing account authority through sync_accounts), and per-buy reporting_webhook and account-level subscribers are independent of the principal-level subscriber.
The principal.changed webhook
StreamHaus tells the agent’s subscriber when its own transitions change the principal state. The payload is an invalidation signal, not state: identifiers and a repair pointer only. The receiver verifies the webhook signature, dedupes on idempotency_key, and repairs by calling get_principal.
Schema: principal-changed-webhook.json
$schema key above is documentation metadata, not part of the payload. The webhook is not fired for the agent’s own sync_principal mutations, since the sync response already reports those. Webhooks can be dropped, so a receiver also polls get_principal on a bounded schedule rather than relying on the doorbell alone.
Run it against the training agent
The public training agent implementssync_principal and get_principal on its sales endpoint, https://test-agent.adcontextprotocol.org/sales/mcp (see Build a caller for connecting). Calls need a credential: anonymous calls get AUTH_REQUIRED. It is a reference seller, so it differs from StreamHaus:
- It advertises the
reporting_destinationsanddeclarationssections. Webhook subscribers are still registered throughsync_agent_notification_configs, so dropnotification_configsfrom the step 4 request. - Destination proof is simulated. An active destination reads back
action_requiredwithsetup.action: prove_control(also for warehouse patterns, where a real seller would ask forgrant_access), thenreadya few seconds later. No bucket is contacted and noprincipal.changedwebhook fires. - It offers
file_transfer(s3,gcs) andwarehouse_materialization(bigquery,snowflake). Read itsadcp.principalblock for the exact offering. - State is keyed to the credential you authenticate with, and everyone using the shared public test token is the same principal. Treat anything you register there as public, and expect
currentrather thanrecognizedif someone else has already configured it.
destination_id so the sequence is reproducible on a shared token:
Schema: sync-principal-request.json
get_principal first, send this request (expect applied with action_required), then poll get_principal until the destination is ready. Send the same body under a fresh idempotency_key to see action: "unchanged" with the same destination_ref. Clear your destination by resubmitting the section without it; avoid [] on a shared token unless you registered everything there. The principal storyboard covers the recognized read, apply and readback, idempotent replay, and section clearing; it applies a suspended destination, so it does not exercise the proof transition.
Failure branches
The full error table is in thesync_principal reference. The ones a buyer agent meets first:
AUTH_REQUIRED. The call carried no stable principal. Authenticate with a credential the seller maps to one.UNSUPPORTED_FEATURE. A section, destination, or event type is outside what the seller advertised. Re-read capabilities and choose from the offering.CONFLICT.expected_configuration_versionorexpected_principal_kinddid not match. Nothing changed; read withget_principal, reconcile, and resubmit under a new key.- A destination stays
action_required. The typed action is incomplete or unverified. Complete it in the provider and poll; resubmit only ifsetup.expires_athas passed. - A destination is
rejected. Those coordinates cannot be used. Register a different destination rather than retrying. - The feed binding is refused. The destination is not
ready, or the account relationship does not authorize the feed. Fix whichever side failed; one does not substitute for the other.
Related reference
sync_principalandget_principal— request, response, and error referenceget_adcp_capabilities— theadcp.principalblocksync_accounts— account authorization andreporting_delivery_configs- Provision a seller-mediated account — the related account walkthrough
sync_agent_notification_configs— the specialized webhook registration task