> ## Documentation Index
> Fetch the complete documentation index at: https://docs.adcontextprotocol.org/llms.txt
> Use this file to discover all available pages before exploring further.

# sync_accounts

> sync_accounts provisions or links advertiser accounts with an AdCP seller agent for one or more brand/operator pairs. Supports explicit and implicit account models with sandbox mode.

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`](/dist/docs/3.0.28/accounts/tasks/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`](/dist/docs/3.0.28/accounts/tasks/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/3.0.28/account/sync-accounts-request.json`](https://adcontextprotocol.org/schemas/3.0.28/account/sync-accounts-request.json)
**Response Schema**: [`/schemas/3.0.28/account/sync-accounts-response.json`](https://adcontextprotocol.org/schemas/3.0.28/account/sync-accounts-response.json)

## Quick start

Sync a single advertiser account and check the resulting status:

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { testAgent } from "@adcp/client/testing";
  import { SyncAccountsResponseSchema } from "@adcp/client";

  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}`);
    }
  }
  ```

  ```python Python theme={null}
  import asyncio
  from adcp.testing import test_agent

  async def main():
      result = await test_agent.simple.sync_accounts(
          accounts=[
              {
                  "brand": {"domain": "acme-corp.com"},
                  "operator": "acme-corp.com",
                  "billing": "operator",
              },
          ],
      )

      if hasattr(result, 'errors') and result.errors:
          raise Exception(f"Operation failed: {result.errors}")

      for account in result.accounts:
          print(f"{account.brand['domain']}: {account.status}")
          if account.status == 'pending_approval' and hasattr(account, 'setup') and account.setup:
              print(f"  Complete setup at: {account.setup.url}")

  asyncio.run(main())
  ```
</CodeGroup>

## Request parameters

| Parameter | Type | Required | Description |
| - | - | - | - |
| `accounts` | array | Yes | Array of account entries to sync (see below). |
| `delete_missing` | boolean | No | When true, accounts previously synced by this agent but not in this request are deactivated. Scoped to the authenticated agent. Default: `false`. |
| `dry_run` | boolean | No | When true, preview what would change without applying. Default: `false`. |
| `push_notification_config` | object | No | Webhook for async notifications when account status changes (e.g., `pending_approval` transitions to `active`). |

**Account entry fields:**

| Field | Type | Required | Description |
| - | - | - | - |
| `brand` | object | Yes | Brand reference identifying the advertiser. Contains `domain` (house domain where brand.json is hosted) and optional `brand_id` (for multi-brand houses). See [brand-ref](/dist/docs/3.0.28/brand-protocol/brand-json). |
| `operator` | string | Yes | Domain 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. |
| `billing` | string | Yes | Who should be invoiced: `operator`, `agent`, or `advertiser`. Check `get_adcp_capabilities` for `supported_billing` to see what the seller accepts. The seller must either accept this billing model or reject the request. |
| `billing_entity` | object | No | Structured 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](/dist/docs/3.0.28/building/by-layer/L2/accounts-and-agents#billing-entity-and-invoice-recipient). |
| `payment_terms` | string | No | Payment 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. |
| `sandbox` | boolean | No | When 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`. |

**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:**

| Field | Description |
| - | - |
| `brand` | Echoed from request. Object with `domain` and optional `brand_id`. |
| `operator` | Echoed from request. |
| `name` | Seller's display name for the account. |
| `action` | What happened: `created`, `updated`, `unchanged`, or `failed`. |
| `status` | Current state of the account (see [Account status](#account-status)). |
| `billing` | Billing model applied. Matches the requested value. |
| `billing_entity` | Business 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_scope` | How 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](/dist/docs/3.0.28/building/by-layer/L2/accounts-and-agents#account-scope). |
| `setup` | Present when `status: "pending_approval"`. Contains `url` for completing credit or legal setup, `message` explaining what's needed, and optional `expires_at`. |
| `rate_card` | Seller-assigned rate card identifier (when applicable). |
| `payment_terms` | Payment 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_limit` | Maximum outstanding balance as `{amount, currency}`. |
| `errors` | Per-account errors (only present when `action: "failed"`). |
| `warnings` | Non-fatal notices. |
| `sandbox` | Whether this is a sandbox account, echoed from the request. Only present for implicit accounts. |

### Account status

| Status | Meaning | Next step |
| - | - | - |
| `active` | Ready to use | Use [account reference](/dist/docs/3.0.28/building/by-layer/L2/accounts-and-agents#account-references) in protocol operations |
| `pending_approval` | Seller reviewing | Human may need to visit `setup.url` to complete credit or legal process. Poll `list_accounts` for updates. |
| `rejected` | Seller declined the request | Review rejection reason in `warnings`, adjust and retry, or contact seller |
| `payment_required` | Credit limit reached or funds depleted | Add funds or increase credit limit. Route spend to other accounts. |
| `suspended` | Was active, now paused | Contact seller to resolve |
| `closed` | Was 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.

```json theme={null}
{
  "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`](/dist/docs/3.0.28/accounts/tasks/list_accounts) to check for status changes.

## Common scenarios

### Agency syncing multiple brands

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { testAgent } from "@adcp/client/testing";
  import { SyncAccountsResponseSchema } from "@adcp/client";

  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
    }
  }
  ```

  ```python Python theme={null}
  import asyncio
  from adcp.testing import test_agent

  async def main():
      result = await test_agent.simple.sync_accounts(
          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 hasattr(result, 'errors') and result.errors:
          raise Exception(f"Operation failed: {result.errors}")

      for account in result.accounts:
          if account.status == 'active':
              print(f"Ready: {account.brand['domain']}/{account.brand.get('brand_id')} → {account.status}")
          elif account.status == 'pending_approval':
              print(f"Setup required for {account.brand.get('brand_id')}: {account.setup.url}")
              # Poll list_accounts until status becomes active

  asyncio.run(main())
  ```
</CodeGroup>

### Direct brand purchase

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { testAgent } from "@adcp/client/testing";
  import { SyncAccountsResponseSchema } from "@adcp/client";

  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
  }
  ```

  ```python Python theme={null}
  import asyncio
  from adcp.testing import test_agent

  async def main():
      result = await test_agent.simple.sync_accounts(
          accounts=[
              {
                  "brand": {"domain": "acme-corp.com"},
                  "operator": "acme-corp.com",
                  "billing": "operator",
              },
          ],
      )

      if hasattr(result, 'errors') and result.errors:
          raise Exception(f"Operation failed: {result.errors}")

      account = result.accounts[0]
      if account.status == 'active':
          print(f"Ready: {account.brand['domain']} — {account.status}")
      elif account.status == 'pending_approval':
          print(f"Setup required: {account.setup.url}")
          # Poll list_accounts until status becomes active

  asyncio.run(main())
  ```
</CodeGroup>

### Handling rejection

When a seller declines a request, the account entry has `status: "rejected"`:

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { testAgent } from "@adcp/client/testing";
  import { SyncAccountsResponseSchema } from "@adcp/client";

  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(", ")}`);
      }
    }
  }
  ```

  ```python Python theme={null}
  import asyncio
  from adcp.testing import test_agent

  async def main():
      result = await test_agent.simple.sync_accounts(
          accounts=[
              {
                  "brand": {"domain": "acme-corp.com", "brand_id": "clearance"},
                  "operator": "acme-corp.com",
              },
          ],
      )

      if hasattr(result, 'errors') and result.errors:
          raise Exception(f"Operation failed: {result.errors}")

      for account in result.accounts:
          if account.status == 'rejected':
              print("Account request was rejected")
              warnings = getattr(account, 'warnings', None)
              if warnings:
                  print(f"Reason: {', '.join(warnings)}")

  asyncio.run(main())
  ```
</CodeGroup>

## Error handling

| Error Code | Description | Resolution |
| - | - | - |
| `ACCOUNT_NOT_FOUND` | Referenced account does not exist or is not accessible | Check `account_id` or re-sync |
| `BILLING_NOT_SUPPORTED` | Seller does not support the requested billing model | Check `get_adcp_capabilities` for `supported_billing`, adjust or omit `billing` |
| `PAYMENT_TERMS_NOT_SUPPORTED` | Seller does not accept the requested payment terms | Omit `payment_terms` to accept the seller's default, or negotiate offline |
| `PAYMENT_REQUIRED` | Account has reached its credit limit | Add funds or route to another account |
| `ACCOUNT_SUSPENDED` | Account is suspended | Contact seller to resolve |
| `BRAND_REQUIRED` | Billable operation attempted without brand reference | Include `brand` in the request |

## Next steps

* [list\_accounts](/dist/docs/3.0.28/accounts/tasks/list_accounts) -- Poll for status changes on pending accounts
* [sync\_governance](/dist/docs/3.0.28/accounts/tasks/sync_governance) -- Sync governance agents to accounts
* [Accounts and agents](/dist/docs/3.0.28/building/by-layer/L2/accounts-and-agents) -- Billing models, trust models, and authorized operators
* [Brand protocol](/dist/docs/3.0.28/brand-protocol/brand-json) -- How seller agents resolve brand identity from the `brand.domain`
* [get\_adcp\_capabilities](/dist/docs/3.0.28/protocol/get_adcp_capabilities) -- Discover `supported_billing` and `require_operator_auth` before syncing accounts


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.