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

# Buyer Account Mapping

> Review one seller relationship per operator and brand, inspect private inventory-source coverage, and securely import normalized source-mapping files

Buyer Account Mapping gives a seller one consolidated record for each exact
operator-and-brand relationship, such as a brand buying through its agency. An
authorized buyer sees one account for that relationship, rather than one per
inventory source. The seller can privately see whether each source has a current
mapping.

## Where to find Buyer Account Mapping

In the seller workspace, open **Buyers**, then select **Buyer Account
Mapping**. This opens the full mapping Page without requiring a chat request.

For one buyer relationship, open **Advertisers**, select its External
advertiser row, then open **Account**. That opens the same relationship's
focused account-setup case.

Inventory Sources and ad-server connection settings manage the source
connection and its roster health. Buyer account mapping is configured from
Buyer Account Mapping or the focused Account case, not from the source
connection.

Open the portable Page with the storefront MCP tool:

```json theme={null}
{
  "name": "get_seller_accounts",
  "arguments": {}
}
```

## What is canonical

The relationship is identified by the storefront, operator domain, and brand
domain. A CRM record, GAM advertiser, FreeWheel reference, or another native ID
can support that relationship, but none replaces it.

This keeps two native accounts with the same literal ID separate. For example,
`1234` under `gam-primary` and `1234` under `freewheel-ctv` are different source
mappings even when both support the same buyer relationship.

## How account requests resolve

Each `sync_accounts` request has one resolution authority. A storefront that
builds or mixes inventory resolves the request through its own intake policy. A pure pass-through storefront delegates it to that
agent. Apostra does not run local intake and then forward the same request, or
broadcast it to several possible account authorities.

Before an external mutation, the storefront persists a non-authorizing authority
reservation. Existing grants or seller-owned intakes keep retries on their original
local read-back path even if storefront topology changes.

Delegated decisions are stored locally as an observed-state mirror so buyers can
read pending, accepted, and rejected status consistently. The mirror is not a
second approval: upstream-owned pending requests never appear in the local
seller review queue, and one accepted upstream decision creates at most one
local account authorization.

When an external `sync_accounts` call is asynchronous, the storefront returns a
buyer-pollable task and immediately refreshes that authority with `list_accounts`.
One unambiguous exact account row from the refresh settles the task. Several native
accounts with the same operator, brand, and sandbox remain mapping candidates and do
not decide buyer access. External authorities that
accept asynchronous work must explicitly advertise a reconcilable account roster.
The storefront verifies that capability before forwarding the request, and a
missing or unknown upstream account status never creates access.

If the configured authority is missing, unavailable, or ambiguous, the request
fails before creating access or changing a source mapping. A future account-level
CRM/MCP resolver follows the same rule: it returns exact mapping evidence into the
canonical relationship and source-mapping records; it does not create a parallel
CRM-owned mapping. Existing manual mappings and mappings owned by a different feed
remain protected from overwrite.

## Page views

* **Accounts** shows each consolidated relationship and linked buyer access.
* **Source coverage** expands the relationship across active inventory sources.
  Coverage is `Mapped`, `Not required`, `Not set up`, `Needs account selection`, or `Stale`.
  Source names and native IDs remain seller-private.
* **Sources** shows the last account-listing attempt, last complete snapshot,
  observed/missing counts, and whether Apostra or an upstream sales agent owns the
  account lifecycle. Sellers can refresh a supported source without discarding the
  last complete snapshot when the new listing fails or is incomplete.

The Page is paginated and searchable. It does not expose private source topology
or source-native account IDs to buyers.

## Account setup in Advertisers

To see buyers, open the seller **Advertisers** tab: each buyer is an External
advertiser row, and the **Needs you** filter shows rows awaiting your setup
decision. The tab carries the same account-setup status on External roster rows
with an unambiguous seller-account relationship
that appears in Buyer Account Mapping. Each row also carries one of three
classes — House, Self-serve, External — matching the House/Partner-managed/
Self-serve lens exactly: External is any counterparty reached through an AdCP
account relationship (whether that relationship is Apostra-managed or
runs elsewhere; a cross-organization grant renders with the same External
treatment). The **Needs you** filter isolates every row across lenses with an
open case awaiting your decision — there is no separate inbound-requests inbox;
a pending buyer is a roster row like any other.

An External row opens **Media buys** by default, scoped to that exact seller
relationship: the buyer's activity with the seller — its approval work and
active buys, plus synced creative state. It never shows the buyer's own
campaigns. Delivery figures remain in the seller's Media buys and Reporting
views.
Its **Account** tab opens the same
relationship's account-setup case drawer available from Buyer Account Mapping.
The landing gate is class-only: never by whether a relationship id is present
and never by account-setup state. A pending case changes only what the Account
drawer opens to. If a relationship has not resolved yet, the row does not fall
back to the buyer's Campaigns container. Seller storefronts retrieve this projection from `GET
/api/v2/organization/advertisers/account-setup`.

* **Needs setup — waiting on you** means the case is blocked on the seller;
  you provide or confirm the required setup details.
* **Waiting on buyer** means a live request for information is with the buyer;
  the buyer needs to respond.
* **Awaiting billing** means the relationship has a grant requiring payment and
  billing readiness is not established; it clears when a valid ready-to-invoice
  assertion for that billing entity reactivates the grant.

These are neutral setup statuses, not storefront-health errors. Rows with a
closed or inapplicable case do not show a setup status. House and Self-serve
advertisers do not carry a buyer account-setup status — a Self-serve row is
still an advertiser this seller operates under its own roster (same landing
and jobs as House), not a relationship with a setup case.

## Money view

Once a relationship starts trading, the directory leads with what it's worth
instead of what's left to set up. Both `openProposalCount` and `mtdSpend`
are computed for every relationship in every phase — a relationship still
finishing setup can genuinely have an open demand-inbox proposal before its
account is ever accepted, since a buyer's brief can arrive before that. The
Page simply doesn't lead with these figures until the relationship starts
trading; before then it leads with what's left to set up instead. The
Page's totals summarize every relationship on the storefront, not just the
current page.

| Field                       | Type            | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| --------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `items[].openProposalCount` | integer \| null | Open (undecided) [demand inbox](/v2/storefront/demand-inbox) proposals for this exact operator/brand relationship — the same predicate the demand inbox itself uses to decide "open," so this count and the inbox's own list always agree. Only counts a run whose latest response carries an actual proposal (a non-empty proposal ID); a product offer, an explicit no-offer, or an unavailable/failed response is a real answer but not a proposal, and is excluded. Null only when currently unavailable, never a fabricated `0`. The demand-inbox ledger has no sandbox distinction, so a live and a sandbox relationship sharing the same operator/brand domains read the same count; `totals.openProposals` still counts the ledger total once, not once per relationship.                    |
| `items[].mtdSpend`          | object \| null  | This relationship's month-to-date delivered spend, `{ amount, currency }`, net as seller-reported. Attributed per media buy to this exact relationship via the authoritative routing record behind it (never a buyer/brand heuristic), so it can't be mistaken for a different relationship sharing the same buyer or brand. `currency` is the source's own reported delivery currency, verified against the underlying delivery data itself — never assumed from the buyer's confirmed currency, which can genuinely differ from what the source actually delivered in. Null when this relationship placed no attributable media buy this month, its media buys report more than one source currency (or a buy's own currency couldn't be verified at all), or the figure is currently unavailable. |
| `totals.buyersCount`        | integer \| null | Distinct operator domains across every non-rejected relationship on this storefront (a relationship whose every grant is rejected isn't counted as a buyer). Null when currently unavailable.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `totals.openProposals`      | integer \| null | The demand-inbox ledger's open-proposal count (see `items[].openProposalCount` for what counts as a proposal) for every distinct operator/brand domain pair on this storefront, summed once per domain pair, not just the current page. **Not** the sum of `items[].openProposalCount`: a live and a sandbox relationship sharing the same domains both report that pair's full count on their own item, so summing per item would double it. Null when currently unavailable.                                                                                                                                                                                                                                                                                                                       |
| `totals.activeThisMonth`    | object \| null  | Sum of every relationship's `mtdSpend` on this storefront — each attributed media buy counts toward exactly one relationship, so this never double-counts. `currency` follows the same source-delivery-currency rule as `items[].mtdSpend`. Null when the resolved relationships span more than one currency — summed money never mixes currencies — any relationship had real spend whose own currency couldn't be verified, or the figure is currently unavailable.                                                                                                                                                                                                                                                                                                                                |

## When an account listing does not complete

Only a complete reading of a source's account roster replaces the last complete
snapshot. An attempt that stops short records a diagnostic and leaves the previous
snapshot in place, so a partial read never archives accounts that still exist.

The diagnostic names **whose** limit stopped the attempt, because the remedy is
different in each case:

* `INCOMPLETE_LISTING` — the source itself answered with a partial roster, or
  reported an error while reading it. Check the ad server or sales agent: a
  permission that no longer covers the whole roster is the usual cause.
* `ROSTER_DRAIN_TIMED_OUT` — Apostra ran out of its own time budget before
  finishing the roster read. Scheduled attempts get a longer budget than a
  refresh you trigger by hand, so this often clears on its own within the next
  polling cycle.
* `ACCOUNT_LISTING_FAILED` — the roster read itself failed. Inspect the inventory
  source connection and its credentials.

Apostra follows pagination to the source's terminal page and does not impose an
account-count ceiling. Large complete rosters are published in database batches.

A source that has never once produced a complete snapshot has no accounts to
map, so no buyer relationship can reach `Mapped` coverage on it — and for a
provider that requires an explicit account, that leaves the source unable to
execute. Apostra alerts on that state rather than retrying silently.

### Suggested candidates and the full source roster

When a source's coverage is `Needs account selection` (ambiguous) or `Not set
up` (missing), `list_seller_account_relationships` /
`GET /api/v2/storefront/account-mappings` includes up to 5 suggested
candidates per coverage entry — native accounts that report the same
operator, brand, and sandbox as the relationship — as a `candidates` array
alongside the existing `candidateCount`. `candidateCount` is always the true,
uncapped match count; `candidates` is only a preview for an inline picker.

To search beyond those 5, or when nothing matches automatically, page or
search the source's complete observed roster with [Search a source account
roster](/v2/storefront/account-mappings/tasks/list-source-roster).

## Review pending buyer access

When a buyer requests access to a relationship that requires seller approval,
the request stays pending until a storefront administrator decides it. List the
pending requests with `list_seller_account_grant_reviews` in storefront MCP or
`GET /api/v2/storefront/account-mappings/reviews` in the storefront REST API.
Each row includes the current `version` needed to prevent a decision against
stale review state, the snapshotted policy result, CRM-match status, and the
canonical `intakeId`. The released `grantId` field remains as a deprecated alias
for compatibility; a pending review is not an authorization grant.

Seller relationship rows expose each pending request's resolution authority,
requested billing path, `billingReady`, and truthful `allowedActions`. Direct
operator or advertiser billing cannot be accepted until its billing entity is
resolved; `link_existing` remains available to supply that verified entity.

Use `decide_seller_account_grant_review` or
`POST /api/v2/storefront/account-mappings/reviews/{grantId}/decision` with
`decision: "accept"` to accept using the billing path already resolved for the
request. The released `approve_interchange` value remains a compatibility alias
for agent billing. `link_existing` is the legacy direct-billing acceptance path:
it requires an operator or advertiser billing entity, but it does not create a
CRM link or inventory-source mapping. Rejections require a reason.

Acceptance creates buyer access. Native account creation and source mapping are
separate operations, and existing mappings keep their state unless an
administrator explicitly changes them.

After access is approved, decide coverage separately for every source the account
may use. The Source coverage view is the seller's readback of those decisions; it
does not edit them in this release. An authenticated seller integration can map an
authenticated source-native account or confirm `Not required` when that source's
contract is intentionally unscoped. The REST operation is
`POST /api/v2/storefront/account-mappings/accounts/{accountId}/sources/{inventorySourceKey}/decision`.
Use `expectedVersion: 0` for the first decision and the currently displayed coverage
version for a replacement. A stale version is rejected instead of silently changing
the source used for products or buys.

For example, a modular source that does not consume a native advertiser account is
made eligible explicitly—not inferred from its execution type:

```json theme={null}
{
  "expectedVersion": 0,
  "decision": "not_required"
}
```

### Use Apostra default

For an ad-server-backed source, the account picker can offer **Use Apostra
default** instead of mapping a dedicated account: it maps the relationship to
your shared default advertiser on that connected ad server, rather than
creating or choosing an advertiser for this buyer alone.

This option is offered only when every active grant on the relationship
settles through **agent** (Apostra-cleared) billing and the source's
configured default advertiser currently reads back healthy. Direct-billed
(operator or advertiser) relationships never see it — a direct-billed buyer
needs its own advertiser so invoicing and reporting resolve to that buyer, not
the shared default. Read `GET
/api/v2/storefront/account-mappings/accounts/{accountId}/sources/{inventorySourceKey}/interchange-default-eligibility`
before offering the choice; an ineligible response carries a machine-readable
reason and never a raw boolean.

Choosing it records the mapping through the same decision endpoint as a
dedicated account, with `bindingMode: "shared_default"` and no
`sourceExternalAccountId` — Apostra resolves the native account
server-side, so this option never exposes the shared advertiser's native ID.

Apostra keeps the mapping healthy going forward. If the ad server's
configured default advertiser later changes, or the ad server stops reporting
it, the mapping is marked stale and execution pauses on it until it is either
reconciled against the new default or you choose a dedicated account instead
— it never silently keeps executing against the old default or falls back to
one automatically. A dedicated mapping is unaffected by a default-advertiser
change elsewhere on the source.

## CSV exports and imports

The Page can download:

* an empty `source_account_bindings.csv` v1 template;
* current healthy source mappings; and
* active native accounts with their source namespace, generation,
  status, and listing version.

The normalized mapping columns are:

```text theme={null}
schema_version,operation,operator_domain,brand_domain,inventory_source_key,source_external_account_id
```

Spreadsheet-formula leads are neutralized in every exported field.

Select **Import feed** from the Page to open the portable import Task. The Task:

1. lets an administrator choose the inventory sources covered by the file;
2. uploads the bytes directly to private, short-lived storage;
3. validates the exact schema, hashes, row count, relationships, source accounts,
   and current mapping authority;
4. shows creates, updates, archives, unchanged rows, and quarantined rows; and
5. applies the reviewed set in one atomic, version-checked commit.

Sandbox relationships use the same reviewed mapping flow as live relationships.
If a storefront has both a sandbox and live relationship with the same operator and
brand domains, the Task quarantines that row instead of guessing which one to map.

Each successful commit records the administrator, revision, and impact in the
seller's activity audit. Audit delivery is durable and does not make the mapping
transaction depend on a second service being available at commit time.

Snapshot imports treat the selected sources as complete. A missing row can archive
only a mapping that an earlier revision of the same feed created; manual mappings and
mappings owned by another feed are never removed. Any quarantined snapshot row blocks
the commit. Delta imports require the last committed revision ID and may apply only
accepted rows after the administrator reviews the quarantine impact.

<Warning>
  This release imports only `source_account_bindings.csv`. It does not create
  CRM records, canonical operator-and-brand relationships, grants, or native
  accounts. Exported native-account choices must already come from an
  authenticated inventory source. Files are limited to 10 MB and 10,000 data
  rows.
</Warning>

The MCP Task launcher is:

```json theme={null}
{
  "name": "import_seller_account_feed",
  "arguments": {
    "coveredInventorySourceKeys": ["gam-primary"]
  }
}
```

The upload, preview, and commit capabilities are app-only; they do not appear in the
model's tool catalog.

## CRM and commercial policy

A CRM is optional evidence and workflow acceleration. Runtime product discovery,
execution, and reporting use the inventory-source mapping for the relevant leg.
Credit limits, billing approvals, and permissions remain in their owning typed
commercial contracts; the account-mapping export does not copy or interpret
arbitrary CRM fields.

Native mapping support is runtime-specific. Third-party agent-backed sources and managed
Google Ad Manager or FreeWheel sources consume explicit native-account mappings.
Sellers can refresh those sources' authenticated advertiser choices before exporting
or importing mappings. Third-party `list_accounts` results retain the upstream
operator, brand, status, and freshness needed for diagnosis, but Apostra does not
offer upstream approval or creation actions. A partial, failed, duplicate, or
non-advancing refresh keeps the previous complete choices active and never archives
an omitted advertiser. Automatic polling includes managed sources and external
agents that explicitly advertise `list_accounts`. Account-capable modular sources
can be refreshed on demand; capability-aware periodic modular scheduling remains a
follow-on so unsupported compositions are not retried forever. Modular sources
can also consume mappings when their active modules declare one shared account
namespace across products, media buys, delivery, and account resources. CitrusAd
is the first supported composition and maps a supplier team. The Page labels an
incomplete composition as unsupported; it may participate only through an
explicit, contract-valid `Not required` decision where account scope is not
needed.

## Asking the buyer for more information

Some intakes need something from the buyer before you can decide them — a
completed W-9, a signed contract, an answer to a credit question. Rather than
guessing or rejecting outright, request it: the buyer's account stays
`pending_approval`, now carrying a message and a link into your own process
(a form, a DocuSign envelope, a portal). The buyer's agent presents the
message as untrusted text and offers the link to an authorized human — it
never treats the message as instructions, and it never opens the link
automatically.

Opening the link is not completion. You confirm explicitly once the material
actually arrives, and only then does the case return to your queue ready for
a decision.

If you no longer need what you asked for, withdraw the request instead of
declining the whole case — the case stays open and returns to your queue,
the same way an expired request re-blocks on you.

<CardGroup cols={2}>
  <Card title="Request buyer information" href="/v2/storefront/buyer-account-mapping/tasks/request-buyer-information" icon="circle-question">
    Ask for what you need, with an expiration
  </Card>

  <Card title="Confirm buyer information" href="/v2/storefront/buyer-account-mapping/tasks/confirm-buyer-information" icon="circle-check">
    Record that it arrived
  </Card>

  <Card title="Withdraw an information request" href="/v2/storefront/buyer-account-mapping/tasks/withdraw-buyer-information-request" icon="circle-minus">
    Drop the ask without deciding the case
  </Card>
</CardGroup>

## Billing readiness for direct-billed accounts

Accepting or linking a buyer decides admission — whether they get in. For
**operator** or **advertiser** billing, that is a separate question from
whether you can actually invoice them: your finance system (CRM, ERP, a staff
member, or an external workflow) has to explicitly confirm the buyer is
billable before their account can place direct-billed media buys. Merely
creating a CRM account or a native advertiser is never itself that
confirmation. **Agent** billing (Apostra settlement) never needs this —
Apostra handles collection, not you.

An operator/advertiser-billed account you've accepted or linked but not yet
confirmed billable stays `pending_approval` — no account is created until
readiness exists. Your decision is remembered; asserting readiness completes
it immediately, activating the account without you having to decide again.
Revoking readiness on an account that is already `active` fails closed: it
moves the account to `payment_required`, immediately blocking new
direct-billed activity without touching buys already placed, and it never
silently falls back to agent/Apostra billing on your behalf.

The review queue's `billingGate` field on each pending request shows whether
this gate applies and where it stands: `not_required` (agent billing),
`pending` (never asserted), `ready` (an active assertion exists), or `revoked`
(one existed and was withdrawn). When the gate is `ready` or `revoked`,
`billingGate.assertionId` names the assertion, so you can call the revoke
endpoint below even without keeping the original assert response.

<CardGroup cols={2}>
  <Card title="Assert billing readiness" href="/v2/storefront/buyer-account-mapping/tasks/ready-to-invoice" icon="file-invoice-dollar">
    Confirm a direct-billed buyer is billable, or revoke that confirmation
  </Card>
</CardGroup>
