Skip to main content

Documentation Index

Fetch the complete documentation index at: https://agenticadvertisingorg-changeset-release-main.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Sync advertiser accounts with a seller for one or more brand/operator pairs. The seller provisions or links accounts, returning per-account status and any setup instructions. Brands are identified by a brand object containing domain + optional brand_id, resolved via /.well-known/brand.json. sync_accounts is used across all seller protocols: media buy agents, signals agents, governance agents, and creative agents. It declares the buyer’s intent β€” the seller provisions or links accounts internally. For implicit accounts (require_operator_auth: false), use natural keys (brand + operator) on subsequent requests. For explicit accounts (require_operator_auth: true), discover seller-assigned account IDs via list_accounts. For sandbox on implicit accounts, include sandbox: true in the account entry β€” the seller provisions a test account with no real spend. For explicit accounts, sandbox accounts are pre-existing test accounts discovered via list_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/v3/account/sync-accounts-request.json Response Schema: /schemas/v3/account/sync-accounts-response.json

Quick start

Sync a single advertiser account and check the resulting status:
import { testAgent } from "@adcp/sdk/testing";
import { SyncAccountsResponseSchema } from "@adcp/sdk";

const result = await testAgent.syncAccounts({
  accounts: [
    {
      brand: { domain: "acme-corp.com" },
      operator: "acme-corp.com",
      billing: "operator",
    },
  ],
});

if (!result.success) {
  throw new Error(`Request failed: ${result.error}`);
}

const validated = SyncAccountsResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

for (const account of validated.accounts) {
  console.log(`${account.brand.domain}: ${account.status}`);
  if (account.status === "pending_approval" && account.setup?.url) {
    console.log(`  Complete setup at: ${account.setup.url}`);
  }
}

Request parameters

ParameterTypeRequiredDescription
accountsarrayYesArray of account entries to sync (see below).
delete_missingbooleanNoWhen true, accounts previously synced by this agent but not in this request are deactivated. Scoped to the authenticated agent. Default: false.
dry_runbooleanNoWhen true, preview what would change without applying. Default: false.
push_notification_configobjectNoWebhook for async notifications when account status changes (e.g., pending_approval transitions to active).
Account entry fields:
FieldTypeRequiredDescription
brandobjectYesBrand reference identifying the advertiser. Contains domain (house domain where brand.json is hosted) and optional brand_id (for multi-brand houses). See brand-ref.
operatorstringYesDomain of the entity operating on the brand’s behalf (e.g. pinnacle-media.com). When the brand operates directly, set to the brand’s domain. Verified against the brand’s authorized_operators in brand.json.
billingstringYesWho should be invoiced: operator, agent, or advertiser. Check get_adcp_capabilities for supported_billing to see what the seller accepts at the capability level. The seller must either accept this billing model or reject the request. Sellers MAY additionally reject a value the seller-wide capability accepts when the calling buyer agent’s commercial relationship does not permit it β€” e.g., a buyer agent onboarded as passthrough-only (no payments relationship β€” only the operator can be invoiced). The two gates use distinct error codes β€” BILLING_NOT_SUPPORTED for the seller-wide capability gate, BILLING_NOT_PERMITTED_FOR_AGENT for the per-buyer-agent gate β€” so agents can dispatch on autonomous-retry vs human-onboarding without parsing prose. See Buyer-agent identity and Billing and Account Setup.
billing_entityobjectNoStructured business entity details for the party responsible for payment. Contains legal_name (required), plus optional vat_id, tax_id, registration_number, address, contacts, and bank. Bank details are write-only β€” included in requests but never echoed in responses. See billing entity and invoice recipient.
payment_termsstringNoPayment terms for this account: net_15, net_30, net_45, net_60, net_90, or prepay. The seller must either accept these terms or reject the account β€” terms are never silently remapped. When omitted, the seller applies its default terms.
sandboxbooleanNoWhen true, set up a sandbox account with no real platform calls or billing. Only applicable to implicit accounts (require_operator_auth: false). For explicit accounts, sandbox accounts are pre-existing test accounts discovered via list_accounts.
notification_configsarrayNoAccount-level webhook subscribers for events that outlive any single media buy: creative lifecycle notifications and wholesale feed change webhooks. Omit to leave existing subscribers unchanged; send [] to remove all subscribers; send a full array to replace.
Natural key: The tuple (brand, operator, sandbox) uniquely identifies an account relationship. {brand: {domain: "acme-corp.com"}, operator: "acme-corp.com"} (direct) is a different account from {brand: {domain: "acme-corp.com"}, operator: "pinnacle-media.com"} (via agency). Adding sandbox: true provisions a sandbox account for the same brand/operator pair β€” no real platform calls or billing.

Response

Success response: Returns an accounts 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). No accounts array is present.
Note: Responses use discriminated unions β€” you get either accounts OR errors, never both. Per-account fields:
FieldDescription
brandEchoed from request. Object with domain and optional brand_id.
operatorEchoed from request.
nameSeller’s display name for the account.
actionWhat happened: created, updated, unchanged, or failed.
statusCurrent state of the account (see Account status).
billingBilling model applied. Matches the requested value.
billing_entityBusiness entity details for the invoiced party, echoed from the request. Sellers may add fields the agent omitted (e.g., registration_number from a credit check) but must not return data from a different entity. Bank details are omitted (write-only).
account_scopeHow the seller scoped this account: operator (shared across brands for this operator), brand (shared across operators for this brand), operator_brand (dedicated to this operator+brand pair), or agent (the agent’s default account). See account scope.
setupPresent when status: "pending_approval". Contains url for completing credit or legal setup, message explaining what’s needed, and optional expires_at.
rate_cardSeller-assigned rate card identifier (when applicable).
payment_termsPayment terms agreed for this account: net_15, net_30, net_45, net_60, net_90, or prepay. When the account is active, these are the binding terms for all invoices.
credit_limitMaximum outstanding balance as {amount, currency}.
errorsPer-account errors (only present when action: "failed").
warningsNon-fatal notices.
sandboxWhether this is a sandbox account, echoed from the request. Only present for implicit accounts.
authorizationOptional. The calling agent’s scope grant for this account β€” allowed_tasks, field_scopes, scope_name, read_only. Applies to every vendor agent type (media-buy, signals, governance, creative, brand) β€” the Accounts Protocol surface is shared. Present on created, updated, and unchanged results; omitted on failed results. Vendor agents that support scope introspection SHOULD populate this; media-buy sales agents claiming the attestation_verifier standard scope MUST populate it. Absence means the vendor agent does not advertise introspectable scope; callers MUST NOT infer access from absence. See Caller authorization for the full shape.

Account status

StatusMeaningNext step
activeReady to useUse account reference in protocol operations
pending_approvalSeller reviewingHuman may need to visit setup.url to complete credit or legal process. Poll list_accounts for updates.
rejectedSeller declined the requestReview rejection reason in warnings, adjust and retry, or contact seller
payment_requiredCredit limit reached or funds depletedAdd funds or increase credit limit. Route spend to other accounts.
suspendedWas active, now pausedContact seller to resolve
closedWas active, now terminatedβ€”

Async notifications

When push_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). The notification payload includes the (brand, operator) natural key so the buyer can correlate it to the original sync request. For explicit accounts (require_operator_auth: true), the notification also includes the seller-assigned account_id once provisioned.
{
  "brand": { "domain": "nova-brands.com", "brand_id": "glow" },
  "operator": "pinnacle-media.com",
  "status": "active",
  "account_id": "acc_glow_001"
}
If the buyer did not provide 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 + billing at the entry root. The seller provisions or upserts accounts. Used for implicit accounts (require_operator_auth: false). This is the shape AdCP 3.0 shipped with.
  • Settings-update mode β€” account (an AccountRef) at the entry root, with brand/operator/billing absent. The seller updates the account’s settable state β€” no provisioning side effects. Used for explicit accounts (require_operator_auth: true) where accounts are pre-provisioned and discovered via list_accounts. Implicit-account sellers MAY also accept this mode for settings updates against accounts they previously provisioned.
Schema enforces the exclusivity via oneOf β€” sending both shapes on the same entry is a validation error. Sellers that don’t implement settings-update mode reject account-keyed entries with UNSUPPORTED_PROVISIONING; sellers that don’t provision (most explicit-account platforms) reject natural-key entries with the same code.

Account-level webhook subscriptions

notification_configs[] carries account-level webhook subscribers for notifications whose lifecycle outlives any single media buy β€” creative.status_changed, creative.purged, wholesale feed change webhooks (product.*, signal.*, wholesale_feed.bulk_change), and future account-scoped events. Distinct from push_notification_config on this task (account-status webhook), which is one-shot transport for sync_accounts’s own async result. 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 to leave existing subscribers unchanged; send [] to remove all subscribers; send a full array to replace. Each entry has:
  • subscriber_id β€” buyer-supplied identifier, unique within the account; echoed on every fire so multi-subscriber accounts can route by endpoint
  • url β€” HTTPS endpoint URL. For wholesale feed subscribers, sellers MUST complete an endpoint activation challenge or equivalent proof-of-control before treating the subscriber as active.
  • event_types[] β€” types the subscriber wants. Only account-anchored types are permitted (today: creative.status_changed, creative.purged, product.created, product.updated, product.priced, product.removed, signal.created, signal.updated, signal.priced, signal.removed, wholesale_feed.bulk_change). Sellers MUST reject any media-buy-anchored type (scheduled, final, delayed, adjusted, impairment) as a per-account validation failure with INVALID_REQUEST or VALIDATION_ERROR in accounts[].errors[], and error.field MUST point at the invalid event_types entry β€” those events belong on a media buy’s push_notification_config.
  • authentication (optional) β€” legacy Bearer or HMAC-SHA256. Omit to use the default RFC 9421 webhook profile. When present, the same signed-registration downgrade-resistance rules as push_notification_config.authentication apply. Credentials are write-only β€” sellers omit them on reads.
  • active (default true) β€” set false to pause a subscriber without removing the registration. For wholesale feed subscribers, sellers MAY persist the config as inactive until endpoint proof-of-control completes.
Example β€” register a buyer-side endpoint plus an audit bus on an explicit account:
{
  "idempotency_key": "f2c4b7d9-6789-49bc-defa-2345678901bc",
  "accounts": [
    {
      "account": { "account_id": "acc_acme_pinnacle" },
      "notification_configs": [
        {
          "subscriber_id": "buyer-primary",
          "url": "https://buyer.example/webhooks/adcp/creative",
          "event_types": ["creative.status_changed", "creative.purged"],
          "active": true
        },
        {
          "subscriber_id": "audit-bus",
          "url": "https://audit.buyer.example/adcp/ingest",
          "event_types": ["creative.status_changed", "creative.purged"],
          "active": true
        }
      ]
    }
  ]
}
Example β€” register a wholesale feed mirror subscriber for wholesale product and signal changes:
{
  "idempotency_key": "a8af8cf1-89bd-41f3-b27d-7ee7e9f8d2e4",
  "accounts": [
    {
      "account": { "account_id": "acc_acme_pinnacle" },
      "notification_configs": [
        {
          "subscriber_id": "wholesale-feed-sync",
          "url": "https://buyer.example/webhooks/adcp/wholesale-feed",
          "event_types": [
            "product.created",
            "product.updated",
            "product.priced",
            "product.removed",
            "signal.created",
            "signal.updated",
            "signal.priced",
            "signal.removed",
            "wholesale_feed.bulk_change"
          ],
          "active": true
        }
      ]
    }
  ]
}
Governance agents registered via 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 notification_configs[] per account with credentials redacted. Wholesale feed notifications are registered here, not through a separate subscription task. The webhook body is wholesale-feed-webhook.json: it carries the changed product, signal, or 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. Receivers MAY apply the payload to local mirrors; use get_products / get_signals with if_wholesale_feed_version to repair missed or distrusted pushes and before binding spend or authority. See wholesale_feed_webhooks for capability declaration and event semantics.

Common scenarios

Agency syncing multiple brands

import { testAgent } from "@adcp/sdk/testing";
import { SyncAccountsResponseSchema } from "@adcp/sdk";

const result = await testAgent.syncAccounts({
  accounts: [
    {
      brand: { domain: "nova-brands.com", brand_id: "spark" },
      operator: "pinnacle-media.com",
      billing: "operator",
    },
    {
      brand: { domain: "nova-brands.com", brand_id: "glow" },
      operator: "pinnacle-media.com",
      billing: "operator",
    },
  ],
});

if (!result.success) {
  throw new Error(`Request failed: ${result.error}`);
}

const validated = SyncAccountsResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

for (const account of validated.accounts) {
  if (account.status === "active") {
    console.log(`Ready: ${account.brand.domain}/${account.brand.brand_id} β†’ ${account.status}`);
  } else if (account.status === "pending_approval") {
    console.log(`Setup required for ${account.brand.brand_id}: ${account.setup?.url}`);
    // Poll list_accounts until status becomes active
  }
}

Direct brand purchase

import { testAgent } from "@adcp/sdk/testing";
import { SyncAccountsResponseSchema } from "@adcp/sdk";

const result = await testAgent.syncAccounts({
  accounts: [
    {
      brand: { domain: "acme-corp.com" },
      operator: "acme-corp.com",
      billing: "operator",
    },
  ],
});

if (!result.success) {
  throw new Error(`Request failed: ${result.error}`);
}

const validated = SyncAccountsResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

const account = validated.accounts[0];
if (account.status === "active") {
  console.log(`Ready: ${account.brand.domain} β€” ${account.status}`);
} else if (account.status === "pending_approval") {
  console.log(`Setup required: ${account.setup?.url}`);
  // Poll list_accounts until status becomes active
}

Handling rejection

When a seller declines a request, the account entry has status: "rejected":
import { testAgent } from "@adcp/sdk/testing";
import { SyncAccountsResponseSchema } from "@adcp/sdk";

const result = await testAgent.syncAccounts({
  accounts: [
    {
      brand: { domain: "acme-corp.com", brand_id: "clearance" },
      operator: "acme-corp.com",
    },
  ],
});

if (!result.success) {
  throw new Error(`Request failed: ${result.error}`);
}

const validated = SyncAccountsResponseSchema.parse(result.data);

if ("errors" in validated && validated.errors) {
  throw new Error(`Operation failed: ${JSON.stringify(validated.errors)}`);
}

for (const account of validated.accounts) {
  if (account.status === "rejected") {
    console.log("Account request was rejected");
    if (account.warnings?.length) {
      console.log(`Reason: ${account.warnings.join(", ")}`);
    }
  }
}

Error handling

Error CodeDescriptionResolution
ACCOUNT_NOT_FOUNDReferenced account does not exist or is not accessibleCheck account_id or re-sync
BILLING_NOT_SUPPORTEDSeller-wide capability gate (supported_billing does not include the value) or per-account-relationship gate; see Billing and Account SetupCheck get_adcp_capabilities for supported_billing, adjust or omit billing; inspect error.details.scope to disambiguate capability vs account scope
BILLING_NOT_PERMITTED_FOR_AGENTSeller-wide capability accepts the value, but the calling buyer agent’s commercial relationship does not (e.g., passthrough-only β€” no payments relationship); see Buyer-agent identityRetry with error.details.suggested_billing (typically operator) when present; when absent, surface to a human β€” the agent cannot extend its own commercial relationship
PAYMENT_TERMS_NOT_SUPPORTEDSeller does not accept the requested payment termsOmit payment_terms to accept the seller’s default, or negotiate offline
ACCOUNT_PAYMENT_REQUIREDAccount has an outstanding balance requiring paymentResolve outstanding balance or route to another account
ACCOUNT_SUSPENDEDAccount is suspendedContact seller to resolve
BRAND_REQUIREDBillable operation attempted without brand referenceInclude brand in the request

Next steps