Skip to content

Support account.stable_account_id (AdCP 3.3): send seller account IDs as buyer, accept them as seller #1478

Open

Description

@bokelley

Context

AdCP 3.3 adds the optional account.stable_account_id capability (adcontextprotocol/adcp#8123). A seller that declares it:

  • MUST return account_id on every non-failed sync_accounts provisioning result;
  • MUST accept { "account_id": "..." } wherever an AccountRef is accepted, including for buyer-declared (natural-key) accounts;
  • MUST NOT change the id for the life of the account.

Buyers SHOULD send that id instead of the natural key. 3.3 also deprecates the natural key as the reference after provisioning, with removal planned for 4.0 (adcontextprotocol/adcp#7850). The TypeScript SDK shipped the buyer side in adcp-client#3222.

adcp (main, pinned to 3.2.1) has neither side yet.

Buyer side (src/adcp/accounts.py, AccountRegistry.prepare)

The registry already keeps the account_id from sync_accounts (AccountRecord.account_id). prepare() deliberately never swaps it in: "Never infer that a returned handle replaces a natural key." That's correct for a 3.2 seller. Requested:

  1. When the seller declares account.stable_account_id: true, prepare() (under auto / strict) rewrites a provisioned natural-key reference to { "account_id": record.account_id } on every account-scoped task. That covers discovery, sync_creatives and media buys, with sync_accounts provisioning and list_accounts filters keeping their authored references. Without the capability, keep today's behavior: an id returned by a 3.2 seller does not prove it's accepted.
  2. A compatibility override, matching TS accountReferenceMode: 'natural-key'.
  3. Registry lookup by account_id, scoped to seller and buyer identity, for webhook and status-repair flows that arrive with only an id.
  4. Note: the existing status gate is correct. It blocks only spend tasks on payment_required and lets sync_creatives through, which matches the spec. Keep that; the TS SDK has a bug here (adcp-client#3223).

Seller side (src/adcp/decisioning/accounts.py)

NaturalKeyAccounts.resolve rejects any account_id reference with ACCOUNT_NOT_FOUND ("Use the provisioned natural key"). A Python seller therefore can't declare stable_account_id without writing its own store. Requested:

  1. An opt-in way for NaturalKeyAccounts to resolve { account_id }. For example, an optional loader_by_id(account_id, auth_info) callback, or a store flag that routes ids through the adopter's lookup. It must resolve only accounts the authenticated caller may operate, so an id never crosses principals. When it's configured, the framework (or docs) should prompt the adopter to declare account.stable_account_id: true and return account_id from upsert_request results.
  2. Both forms must reach the same account. State written under the natural key has to be visible under the id and vice versa. The AdCP training agent hit exactly this split while implementing the capability (feat(training-agent): declare account.stable_account_id on v6 tenant routes adcp#8124).

Schema dependency

stable_account_id exists only from the 3.3 beta schemas; this SDK pins 3.2.1. Until it syncs 3.3, read the capability from the raw capabilities payload (or behind 3.3 support), and treat it as absent for 3.2 sellers.

Acceptance

  • Buyer: with a seller declaring stable_account_id: true, after accounts.ensure(), get_products, sync_creatives and create_media_buy carry { account_id }. A seller without it still gets the natural key.
  • Buyer: override keeps natural keys; lookup by account_id finds the ensure() entry.
  • Seller: NaturalKeyAccounts with id resolution configured resolves { account_id } and the natural key to the same account; another principal's id returns ACCOUNT_NOT_FOUND.
  • Docs for both sides.

Activity

  1. bokelley commented on Nov 8, 2026

    @bokelley
    ContributorAuthor

    Re-scope: account.stable_account_id is deprecated in AdCP 3.3 (get-adcp-capabilities-response.json, x-deprecated-in: 3.3.0, planned for removal in 4.0). Its guarantee is now universal: every seller MUST return account_id from sync_accounts once the account is resolved, accept it on every account-scoped task, and never change it. Sellers SHOULD still send stable_account_id: true while the field exists, and buyers MUST NOT require it before sending account_id. This follows adcontextprotocol/adcp#8474: identity is the brand domain, and an account is its account_id.

    For SDK 10 (next), that means:

    • Buyer: after provisioning, always reference the account by account_id. Don't gate on the capability flag.
    • Seller: always return account_id and accept it everywhere an AccountRef is accepted. Emit account.stable_account_id: true for compatibility.
    • Don't build new logic around the flag; it goes away in 4.0.

    The 9.x line (AdCP 3.2) is unchanged.

    🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestsdk-10SDK 10 (next branch, AdCP 3.3)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions