> ## 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.

# Storefront Onboarding

> End-to-end guide for sellers/publishers to onboard their storefront, register inventory, and start accepting agent traffic

<Note title="Beta">
  The v2 API is in active development. The onboarding flow described here may evolve before general availability.
</Note>

## Explore a sample Storefront before signup

You can open [Explore a sample Storefront](https://app.apostra.com/explore/sample-storefront) before
creating an account. It is a public, stateless walkthrough with versioned,
preloaded fictional data. Its Catalog, fictional buyer brief, and approval path
are display-only: there are no text fields, uploads, saved changes, buyer
messages, pricing actions, or production-system connections.

The sample is not a workspace or a Demo Storefront. It does not create an
account, tenant, source, or demo lease, and it cannot publish inventory, buy,
sell, bill, or contact a buyer. Create a Seller Account and accept the Terms
before adding your organization's material or connecting a production system.

## Confirming your company (Media Company signup)

<Note title="Gradual rollout">
  This pre-application step is enabled for a controlled cohort while Apostra
  validates the underlying research and admission policy. Signups outside the
  enabled cohort continue directly into the flow described in the rest of this
  guide, with no gap in access.
</Note>

Before you reach account setup, Apostra identifies your organization from the
work email you signed up with and asks you to confirm it. This is a separate,
one-time admission step — distinct from **Step 1 "Verify your company"**
below, which resolves your public AAO brand profile after your account
already exists.

1. **Identity proposal.** From your email's domain, Apostra proposes a company
   name and website: *"From `acme.com`, we think you're Acme Media. Is that
   accurate?"* Confirming reuses your verified sign-up email domain as
   evidence of control. Correcting the domain to something else is recorded as
   an unverified claim — a claimed domain is never treated as proof of
   ownership, so an unrelated domain does not by itself grant your account any
   authority over it.
2. **Seller product.** Choose **Just list** or **Create an AMC account**. Just
   list uses an agent operated by you, a partner, or another provider. An AMC
   account includes listing and adds a hosted Merchandising Agent that you
   train for your business. The choice remains editable after onboarding.
3. **Billing country.** You confirm the country of the company that will
   contract with Apostra — not your personal location. This country is the
   sole input to the plan currency Apostra derives and displays back to you;
   there is no currency picker, and an unsupported country never silently
   falls back to USD. If your country isn't supported yet, you're shown an
   assisted path to contact Apostra directly instead of a dead end.
4. **Terms of Service and Privacy Policy.** Baseline platform Terms
   acceptance and Privacy Policy disclosure happen here, before any setup
   content. A paid rate card is a separate, later acceptance once you choose a
   plan.
5. **Research and admission.** Confirming starts an automatic background
   check — normally a few seconds. You'll see a brief "Checking your
   details" state; if it runs long, the page tells you it's still working, and
   you can safely close it — refreshing the page or logging back in always
   resumes the same status rather than restarting the question.

Once admitted, your applicable setup workspace already treats company identity
as complete. Just-list operators continue to the Agents workspace to connect
the agent they or their provider operates; an Agentic Media Company continues
to the inventory-source and merchandising steps below.
Admission at this stage
means your organization may operate a Media Company workspace at all; it is
never a marketplace listing, Critical Supply, or Market Maker decision — those
remain separate reviews later in the storefront lifecycle.

See [Confirm Media Company identity](/v2/storefront/tasks/confirm-media-company-identity)
for the request/response schema, status values, and error codes behind this
step.

## Overview

A **Storefront** is your buyer-facing home on Apostra: the business presence, name, description, and discovery surface buyers use to understand who they are buying from. Your **Merchandising Agent** runs that Storefront, implements the AdCP media-buy workflow, and draws from the inventory sources you connect.

### Choose a commercial package

Signup asks what you are buying from Apostra, and you can change the plan
later:

* **Listing** — get listed on Apostra, connect an AdCP-compliant
  sales agent that you or a provider operates, and keep the operational record
  for campaigns, media buys, creatives, approvals, delivery, and activity. AI
  Business Rules are available. Saving, enabling, and evaluating AI Business
  Rules is not an IU-rated activity today; other qualifying activity remains
  governed by your organization's accepted IU Rate Card.
* **Listing + Distribution** — keep that connected agent while adding self-serve
  advertisers, public listing distribution and an optional customer CNAME, and
  customer-branded AdCP and ChatGPT app channels.

<Note>
  Listing + Distribution is offered only when the active Rate Card publishes it.
  Review and accept the published package price in Plan & billing before it
  takes effect.
</Note>

Signup also shows two private contact paths: **Enterprise — Listing** and
**Enterprise — Merchandising**. Neither is a public or selectable Rate Card
plan. Choose **Get in touch** to discuss either path; your plan, price, and
access do not change unless your organization later accepts a separate private
offer.

This is commercial packaging, not a behavioral mode. The `setupIntent` field
remains a reversible record of the selected signup package for compatibility,
but it never suppresses setup steps, navigation, tools, analytics, or unrelated
capabilities. Listing keeps operational surfaces and AI Business Rules available
and presents **Add Listing + Distribution** for paid distribution capabilities.
Merchandising remains separate and requires a connected source that is ready to
supply products your storefront can sell, such as an ad server — a
wholesale-capable source that has not finished its own setup does not satisfy
this yet. Source treatment remains per source (`WHOLESALE`,
`COMPOSING`, or `BOTH`). Who operates a connected sales agent is Source
configuration, not a different Listing plan or account profile; a third-party
sales-agent source with Listing is simply `COMPOSING`.

### Choose your seller product

Supply-side signup asks which product you want to start with:

* **Just list** — list inventory with an agent operated by you, a partner, or
  another provider. You can connect the agent during setup.
* **AMC account** — listing plus Apostra's hosted Merchandising Agent, which you
  train for your business. The Merchandising Agent can draw from an ad server,
  custom modular source, external Agent, or linked Storefront and uses your
  merchandising guidance to package, price, and sell inventory.

You can change this selection later. It controls which workspaces and setup
requirements apply; it does not classify your company permanently, decide how
an individual Source is treated, or grant a paid merchandising entitlement.
Just list starts by connecting the agent operated by you or your provider;
there is no inventory-source prerequisite for that connection. Agentic Media
Company setup starts by connecting inventory sources and also gathers the
advertiser context that guides the hosted Merchandising Agent.

To switch products after onboarding, open **Settings → How would you like to
sell?** and choose Just list or Agentic Media Company. You can also ask Murph to
make the change. An integration can make the same audited change by sending
`operatingMode` to `PATCH /api/v2/storefront`. The update replaces the listing
and Apostra-merchandising capability pair together, and the applicable
navigation changes without deleting completed setup or changing your plan.

<Warning>
  `PATCH /api/v2/storefront` is a **true partial patch**: a capability flag you
  leave out keeps its stored value. `PUT /api/v2/storefront` is the aggregate
  update and applies the whole capability object, so a flag you leave out of a
  `PUT` is stored as `false`. Use the `PATCH` when you mean to change some flags
  and leave the rest alone.
</Warning>

### What you declare vs. what buyers get

Capability is two values, and they can legitimately disagree:

* **`configuredCapabilities`** — the flags you declared. This is what you wrote,
  and it is the field to compare against when you want to know whether a save
  would change anything.
* **`capabilities`** — the **effective** projection buyers see. Derived from your
  declaration plus your source topology and approval settings.

The derivation rules, all of them observable on the storefront read. Declaring
a flag `false` is never rejected — what differs is whether the *effective*
response reflects what you sent. Declaring a flag `true` can be rejected: see
*Approval-routing prerequisite* below for the exact rule before assuming every
save succeeds.

* **`capabilitiesLocked: true`** — you have ad-server-backed inventory (an
  embedded sales agent). `offersCreativeReview` and `offersCampaignApproval`
  are effectively on regardless of what you declared — the storefront *is* the
  agent buyers address, so those workflows are always live. The lock does
  **not** extend to `offersProductComposition`: it is derived the same way as
  on an unlocked storefront (merchandising entitlement plus a ready wholesale
  source or the ambient wholesale pool), so it can still be effectively off on
  a locked storefront.
* **Product composition off** (no ad-server-backed inventory, no adapter) —
  the storefront routes selling to your external sources on the Agent-supplied
  path, so no Storefront-owned workflow is advertised: creative review,
  campaign approval, and product composition are all effectively off even if
  you declared them on.
* **Product composition on (no ad-server-backed inventory, no adapter)** —
  `offersProductComposition` is on. `offersCampaignApproval` is derived from
  `mediaBuyApproval` (on only when it's `manual`) — your approval setting is
  the single source of truth for whether composed buys queue for review, so
  declaring the flag does not override it. `offersCreativeReview` is exactly
  what you declared.
* **Adapter-routed storefronts** — your declared flags are returned verbatim,
  *except* for the social-platform adapters (LinkedIn, Meta, Pinterest, Reddit,
  Snap, TikTok), where the platform owns creative and campaign acceptance
  end-to-end: `offersCreativeReview` and `offersCampaignApproval` are always
  off regardless of what you declared. `offersProductComposition` is still
  returned exactly as declared on every adapter, social or not.

This is why a successful save is not a promise that buyer-facing capability
changed. Read `capabilities` back after a write, not just the flags you sent,
and treat `configuredCapabilities` as the record of your own declaration.
`setupIntent` is a compatibility record of the signup package, never a runtime
mode. It does not determine source treatment or hide product functionality.

### Approval-routing prerequisite

A save is rejected with a `400` (field-scoped: `mediaBuyApproval` for
media-buy approval, `creativeApproval` for creative review) under one rule,
applied independently for each of the two approval kinds:

> On an existing, non-adapter-routed storefront, if this write results in the
> approval setting (`mediaBuyApproval` or `creativeApproval`) being `manual`
> **and** the matching capability (`offersCampaignApproval` or
> `offersCreativeReview`) being effectively `true`, the write is rejected —
> **unless** that exact pair (approval already `manual` **and** capability
> already effectively `true`) already held immediately before this write, or
> routing already resolves for that kind: either you have an active primary
> approval-routing policy saved for it, or — when none is saved — the default
> routing to active organization admins resolves. Routing is opt-out, not
> opt-in: a storefront with no saved policy is still covered as long as an
> active organization admin exists to receive it.

That predicate is the whole rule — the examples below illustrate it, they do
not define when it can or cannot fire:

* **Plain storefront, turning a workflow on for the first time, with no
  routing at all.** Product composition is effective, `creativeApproval` is
  already `manual`, `offersCreativeReview` goes from `false` to `true` in this
  write, there is no saved creative-review policy, and no active organization
  admin exists to fall back to → rejected.
* **ESA-backed storefront, approval-setting transition.**
  `offersCreativeReview` is always effectively `true` on an ad-server-backed
  storefront, but if a separate write changes `creativeApproval` from `auto`
  to `manual` and routing does not resolve for creative review (no saved
  policy and no active organization admin), it is still rejected — the
  capability's value never changed, but the (approval, capability) pair
  became newly `(manual, true)` together.
* **Already active — resaving is a no-op.** If `mediaBuyApproval` is already
  `manual` and `offersCampaignApproval` is already effectively `true`,
  resaving the same values does not re-trigger the check, because that pair
  already held before the write.

If you have an active organization admin, default routing already covers you;
otherwise, configure a primary approver before turning a workflow on for the
first time or before switching its approval setting to `manual`.

(Source: `apps/api/src/services/v2/storefront.service.ts:1904-1968`; routing
check: `apps/api/src/services/storefront-sources/approval-routing.ts:917-968`.)

For organizations using **Just list** or **Agentic Media Company**, the Storefront
journey has **four user-visible steps**, mirroring the in-app onboarding UI:

1. **Verify your company** — resolve your brand from the AAO registry, set your operator domain, and auto-verify (or fall back to manual KYC).
2. **Connect inventory sources** — register one or more inventory sources: an external sales agent, your own ad server with Apostra-managed sales-agent plumbing, or another Storefront.
3. **Set up settlement and payouts** — confirm the Seller Account currencies used
   for settlement, then add payout bank details so Apostra can pay you by
   bank transfer. Currency is part of go-live readiness. Payout details are
   required to receive disbursements for normal Seller Accounts, but they never
   block launch: funds accrue until the details are added. They are optional
   for official Apostra sales-adapter Seller Accounts that already operate under a
   downstream platform settlement agreement. Seller-cleared settlement for
   normal Seller Accounts is coming later and is not configurable today.
4. **Go live** — satisfy every current readiness requirement, including transaction proof for each active path in the compatibility-named `publish_validation` check. A third-party Sales Agent's current-revision proof is reusable across its Sources; an uncovered Source can run the public validation skill. The derived status becomes `live` automatically while the storefront is not paused; Apostra review remains a separate prerequisite for public buyer discovery.

A few helper endpoints support these steps but are not standalone "steps":

* `POST /resolve-brand` — looks up your brand in the AAO registry. Used inside Step 1 to pre-fill the form.
* `GET /discover-agents` — surfaces agents AAO knows about for your domain. Used inside Step 2.
* `GET /readiness` — the canonical status projection you can call any time to see what gates remain.

<Info title="Where protocol and registry truth lives">
  The AdCP specification and AAO registry define the protocol, registry records,
  storyboards, and validation semantics. This guide explains how Apostra uses
  those signals during setup: AAO **registration** blocks connecting an external
  agent source; AAO **compliance** is surfaced as an advisory warning; publisher
  `adagents.json` **authorization** is surfaced as an advisory setup/product signal
  today; and marketplace listing is a separate Apostra review step after activation.
</Info>

<Tip>
  Each seller account receives **one storefront automatically** when the account
  is provisioned, and each storefront can connect one or more inventory sources.
  There is no `customerId` path parameter — the storefront is resolved from your
  API key's account context.
</Tip>

### Using Apostra app

You do not need to create a storefront before starting setup in Apostra
app; it already exists when your seller account is ready. Open **Business
profile**, choose **Build my profile**, and tell Murph about your business,
inventory, channels, regions, and buyer-facing pitch. Murph will propose the
profile for your confirmation.

The remaining setup areas then guide you through connecting inventory, setting
your selling rules, testing the storefront, and resolving readiness blockers.
Buyer Setup and Seller Setup share the same status, progress, and operator
editing pattern. Seller Setup then adds storefront-only tracks for inventory,
publisher authorization, settlement, and **Get paid**; the last of those stays
visible without being counted as a launch blocker.
Setup time depends on the inventory sources you connect, their authorization
and compliance state, and whether your account is ready for billing and
activation; Apostra does not promise a fixed setup time.
Use the API flow below only when you are integrating programmatically.

### IU plan during the staging pilot

The Organization IU Rate Card is published in staging for a controlled pilot and
visible only to invited organizations. Public seller signup remains closed
until the same `iu-rate-card-pilot` flag is rolled out globally. Invited staging
organizations see the exact Rate Card revision on their next eligible login;
after global rollout, new organizations see it during signup as well. Accepting
it creates an immutable record of the exact revision and plan accepted. An
admitted published activity may then draw from the accepted IU balance and
appear as usage during the pilot. Monetary charging, invoices, payment
collection, renewal charging, and separately controlled entitlement enforcement
remain off.

New Rate Cards show exactly three published activities: **Brief response** (1
IU per completed Apostra merchandising cycle), **Enhanced Reporting** (4
IUs per exact connected account per billing period after its control is enabled
and its existing reporting subscription completes a successful sync), and
**Interchange media buy** (1 IU per
qualifying non-social buy per billing period with positive impressions or
spend). The accepted version remains the authority; earlier accepted Rate Cards
keep their historical activities.

You can instead **Continue without a paid plan** or **Decide later**. Continuing
without a plan suppresses the automatic login prompt only for that exact Rate
Card revision; deciding later allows it to appear again on the next login. The
manual **Choose an IU plan** action remains available under **Settings → Plan &
Billing** while the offer is current. These plan choices are separate from the
payout details required for Apostra-cleared Seller Account settlement.

Plan & Billing shows only public plans that cover every active product in the
billing organization. For a Seller Account, the choices also follow the current
Storefront operating mode: Listing accounts receive listing plans, while Agentic
Media Companies can choose listing or merchandising plans. Buyer, Seller, and
Partner products in the same organization must all be covered by one plan; Apostra
will prepare a targeted offer when no public plan covers that combination. Existing
accepted plans remain in effect until the organization accepts different terms.

## Who this is for

* Publishers and sales houses connecting their inventory to agentic buyers
* Retail media networks exposing on-site or off-site inventory through AdCP-compatible agents
* Any seller who wants buyer agents (e.g. Apostra, Claude, custom buyers) to be able to discover and transact against their inventory

## Prerequisites

<Steps>
  <Step title="Apostra API key">
    Generate a key at [app.apostra.com/user-api-keys](https://app.apostra.com/user-api-keys). Keys start with `scope3_` and authorize all storefront endpoints.
  </Step>

  <Step title="A registered brand on AAO">
    Your brand should have a `brand.json` published and resolvable through the AAO registry at `agenticadvertising.org`. If you don't have one yet, the resolve-brand call returns a `builderUrl` that points you to the registry's brand builder.
  </Step>

  <Step title="At least one inventory source">
    An external AdCP-compatible sales agent, an operator-owned ad server, or another Storefront. For external agents, you'll need the endpoint URL, protocol, and (for non-OAuth agents) auth credentials.
  </Step>

  <Step title="Optional: payout bank details">
    Required for Apostra-cleared settlement. Have your bank details ready — beneficiary name and address, account number or IBAN, one bank identifier (Fedwire/ABA routing number, CHIPS ABA, SWIFT-BIC, or local bank code), and the currency your account accepts. Accounts under an organization inherit billing from the organization and do not set up their own. Seller-cleared settlement for normal Seller Accounts is not configurable yet.
  </Step>
</Steps>

***

## Onboarding flow

<Steps>
  <Step title="Verify your company">
    The first thing a seller does is identify their company so Apostra can pull their brand profile from AAO and validate the operator domain. This step combines a brand lookup, a storefront update, and an automatic operator-domain verification check.

    #### 1. Resolve your brand (helper)

    Look up your brand in the AAO registry to grab the canonical brand name and logo URL. This call has no side effects — it's only used to populate the storefront update payload.

    <CodeGroup>
      ```bash curl theme={null}
      curl -X POST https://api.apostra.com/api/v2/storefront/resolve-brand \
        -H "Authorization: Bearer scope3_..." \
        -H "Content-Type: application/json" \
        -d '{ "domain": "acme.com" }'
      ```
    </CodeGroup>

    ```json Response (resolved) theme={null}
    {
      "resolved": true,
      "domain": "acme.com",
      "brandName": "Acme",
      "logoUrl": "https://cdn.example.com/acme-logo.svg",
      "manifestUrl": "https://acme.com/.well-known/brand.json",
      "manifest": { "...": "full brand.json" },
      "registryEntry": { "...": "AAO registry entry" },
      "authorizedOperators": [
        { "domain": "acme-media.com", "scope": "primary" }
      ],
      "houseBrand": false
    }
    ```

    If no manifest is found, the call returns `200` with `{ "resolved": false, "builderUrl": "https://agenticadvertising.org/brand" }`.

    **What if my brand isn't found?** A `resolved: false` is not an error and does not block you — it just means you haven't published a `brand.json` yet. The storefront shows no resolved brand logo in that state; it never substitutes a logo inferred from your website or a third-party enrichment service. Publish one at the `builderUrl` (or host your own at `/.well-known/brand.json`) and re-run `resolve-brand`; we read it live. Your `brand.json` is your own identity document — we read it, we never own it. See [Identity documents](/v2/concepts/identity-documents) for what it declares and how it differs from publisher authorization (`adagents.json`).

    <Note>
      The `domain` field is validated against a strict FQDN regex. IP addresses and internal hostnames are rejected to prevent SSRF.
    </Note>

    #### 2. Write the brand fields onto your storefront

    Seller account provisioning creates this storefront record automatically. You
    can retrieve it with `GET /storefront`. `POST /storefront` remains idempotent for
    programmatic recovery and returns the existing record rather than creating a
    duplicate:

    <CodeGroup>
      ```bash curl theme={null}
      curl -X POST https://api.apostra.com/api/v2/storefront \
        -H "Authorization: Bearer scope3_..." \
        -H "Content-Type: application/json" \
        -d '{
          "name": "Acme Media",
          "publisherDomain": "acme.com",
          "operatorDomain": "acme.com",
          "plan": "basic"
        }'
      ```
    </CodeGroup>

    `POST /storefront` is idempotent — if a storefront already exists for your account, the existing record is returned instead of creating a duplicate.

    `operatorDomain` is the canonical domain this storefront operates as and the identity buyers and AAO matching use for the storefront. It can differ from the account's registered `customerDomain`, and it can be left unset during setup if the operator is not known yet. A storefront cannot go live until an operator domain is set and verified. `publisherDomain` is optional storefront metadata and should not be used as the matching key for cross-publisher storefronts.

    Then update it with the brand fields from `resolve-brand` plus your operator domain:

    <CodeGroup>
      ```bash curl theme={null}
      curl -X PUT https://api.apostra.com/api/v2/storefront \
        -H "Authorization: Bearer scope3_..." \
        -H "Content-Type: application/json" \
        -d '{
          "operatorDomain": "acme.com",
          "brandName": "Acme",
          "logoUrl": "https://cdn.example.com/acme-logo.svg"
        }'
      ```
    </CodeGroup>

    ```json Response theme={null}
    {
      "platformId": "acme-media",
      "name": "Acme Media",
      "publisherDomain": "acme.com",
      "operatorDomain": "acme.com",
      "brandName": "Acme",
      "logoUrl": "https://cdn.example.com/acme-logo.svg",
      "operatorDomainVerified": true,
      "plan": "basic",
      "status": "PENDING",
      "createdAt": "2026-04-25T12:00:00.000Z",
      "updatedAt": "2026-04-25T12:00:00.000Z"
    }
    ```

    <Tip title="Operator domain auto-verification">
      When you set `operatorDomain`, the API first checks for an exact match against an approved `customerDomain`. If it does not exactly match, Apostra checks the alias/rebrand evidence path below. An Apostra verification request is needed only when neither path verifies the operator domain automatically.

      If your storefront operates under an alias or rebrand domain, such as an `operatorDomain` that differs from your team's email domain, keep the canonical operator domain you want buyers to see. Apostra can auto-verify the alias when your registered account domain is already approved and trusted AAO or `brand.json` evidence connects the account domain and requested operator domain to the same organization or ownership chain. Useful evidence includes AAO registry org linkage, `brand.json` `authoritative_location`/house portfolio entries, and `authorized_operators`. AAO TXT records, `/.well-known/adagents.json`, AAO Partner membership, and website redirects help Apostra review the request, but a redirect is not ownership proof by itself.

      **Your account domain is approved first, and its approval gates everything else.** Approve it by activating a member whose email matches that domain, or by requesting Apostra attestation for it. Until the account domain is approved, alias and rebrand evidence is not evaluated at all — so an unapproved account domain, rather than your published evidence, is a common reason an alias operator domain stays pending.

      The `operator_domain` readiness check names which case applies and who acts next:

      | Readiness reports                                 | Who acts | What to do                                                                           |
      | ------------------------------------------------- | -------- | ------------------------------------------------------------------------------------ |
      | The account has no registered domain              | you      | add the account domain, then get it approved                                         |
      | The account domain is not yet approved            | you      | activate a matching member email, or request Apostra attestation                     |
      | No AAO or `brand.json` evidence links the domains | you      | publish or correct that evidence                                                     |
      | Apostra could not complete the evidence check     | Apostra  | nothing — the lookup failed or timed out, and it retries on the next storefront read |

      The last row is not a finding about your evidence: your published evidence may already be correct, and there is nothing for you to fix.

      The account `customerDomain` and storefront `operatorDomain` are intentionally separate fields. Updating `customerDomain` syncs the storefront operator domain only when the storefront has no operator domain yet or is still mirroring the previous account domain **and** has no populated operator-identity profile. If the storefront has an explicitly different operator domain, or changing a mirrored domain would clear its `description`, `channels`, `membershipStatus`, or `website`, the API preserves the storefront domain. Change it directly with `PUT /storefront`, where you can resupply the new operator's profile or explicitly confirm the reset.
    </Tip>

    <Warning title="Changing an operator domain can reset its old identity profile">
      `description`, `channels`, `membershipStatus`, and `website` describe the
      current operator identity. When `operatorDomain` changes, the API refuses to
      silently clear any populated values among those fields. The validation response
      lists the affected fields.

      Either resupply valid values for the new operator in the same `PUT`, or pass
      `"confirmOperatorDomainProfileReset": true` to clear the unprovided values.
      Explicitly resupplied fields are preserved or replaced; an unchanged
      `operatorDomain` does not reset anything and needs no confirmation.
    </Warning>

    Common identity fields on `PUT /storefront` include `name`, `publisherDomain`,
    `operatorDomain`, `brandName`, `logoUrl`, `logoBackground`, `description`,
    `channels`, `membershipStatus`, and `website`. The endpoint also accepts the
    storefront configuration fields in the API reference. Confirmation fields alone
    do not count as an update; at least one mutation field must be provided.

    `PUT /storefront` also accepts `subtitle`, `supportUrl`, `privacyUrl`, and
    `termsUrl` — a short tagline and support/privacy/terms-of-service URLs for
    this storefront. Unlike `website`, these are not tied to `operatorDomain`
    identity and are never cleared by an operator-domain change. Every
    marketplace channel listing (the ChatGPT app, a future Claude plugin)
    projects these facts read-only instead of collecting a separate copy per
    channel — see [Create a white-label ChatGPT app](/v2/setup/chatgpt-app-setup).
  </Step>

  <Step title="Connect inventory sources">
    An **inventory source** connects a named slot inside your Storefront to something your Merchandising Agent can draw from: an external ADCP-compatible sales agent, an operator-owned ad server with Apostra-managed sales-agent plumbing behind it, or another Storefront. Buyer-side discovery surfaces your Merchandising Agent as the ADCP actor for the Storefront, and discovery or media-buy calls route through the active source behind it.

    #### 1. Discover agents (helper)

    Optional but recommended: see what AAO already knows about your domain. This proxies AAO's operator and publisher endpoints plus your `.well-known/adagents.json`.

    <CodeGroup>
      ```bash curl theme={null}
      curl "https://api.apostra.com/api/v2/storefront/discover-agents?domain=acme.com" \
        -H "Authorization: Bearer scope3_..." \
        -H "x-aao-api-key: aao_..."
      ```
    </CodeGroup>

    The `x-aao-api-key` header is optional. Without it you only get the public registry view. Pass it to also surface storyboard compliance status for agents you operate.

    ```json Response theme={null}
    {
      "domain": "acme.com",
      "operator": {
        "domain": "acme.com",
        "member": { "slug": "acme-media", "display_name": "Acme Media" },
        "agents": [
          {
            "url": "https://agent.acme-media.com/mcp",
            "name": "Acme Media Sales",
            "type": "SALES",
            "compliance": {
              "status": "passing",
              "storyboards_passing": 12,
              "storyboards_total": 12,
              "headline": "All scenarios pass"
            }
          }
        ]
      },
      "publisher": {
        "domain": "acme.com",
        "adagents_valid": true,
        "properties": [ { "id": "acme-app", "type": "mobile_app", "name": "Acme App" } ],
        "authorized_agents": [
          { "url": "https://agent.acme-media.com/mcp", "authorized_for": ["display"] }
        ]
      }
    }
    ```

    Responses are cached server-side for 2 minutes per `(domain, key fingerprint)`. Pass `&refresh=true` to force a re-fetch.

    #### 2. Register an inventory source

    <CodeGroup>
      ```bash curl theme={null}
      curl -X POST https://api.apostra.com/api/v2/storefront/inventory-sources \
        -H "Authorization: Bearer scope3_..." \
        -H "Content-Type: application/json" \
        -d '{
          "sourceId": "acme-sales",
          "name": "Acme Media Sales",
          "executionType": "AGENT",
          "type": "SALES",
          "endpointUrl": "https://agent.acme-media.com/mcp",
          "protocol": "MCP",
          "authenticationType": "API_KEY",
          "auth": { "type": "bearer", "token": "agent_abc123..." },
          "description": "Primary external sales agent for Acme on-site display + CTV"
        }'
      ```
    </CodeGroup>

    **Required when `executionType: "agent"`:** `type`, `endpointUrl`, `protocol`, `authenticationType`. **`auth` is required** for `API_KEY`, `JWT`, and `BASIC_AUTH` agents and **must be omitted** for `OAUTH` and `NO_AUTH`.

    <Warning title="How auth credentials are stored">
      Inventory-source credentials (API keys, Basic usernames/passwords, and JWT private keys) are encrypted at rest and only referenced by an opaque `auth_secret_ref` in the database. They are **never echoed back** in API responses (the response surfaces `authConfigured: true` instead). Never log, screenshot, or commit raw credentials to source control. Rotate immediately if a credential is exposed.
    </Warning>

    <Tabs>
      <Tab title="API_KEY">
        ```json theme={null}
        {
          "authenticationType": "API_KEY",
          "auth": { "type": "bearer", "token": "agent_abc123..." }
        }
        ```

        Token formats `bearer`, `apikey`, and `api_key` are all accepted. The token is encrypted at rest and never echoed back. The source goes to `pending` and auto-activates once its credential is configured. Reachability is reported independently by source health and connectivity checks.
      </Tab>

      <Tab title="OAUTH">
        ```json theme={null}
        { "authenticationType": "OAUTH" }
        ```

        Omit the `auth` field entirely. The response includes an `oauth.authorizationUrl` you must redirect the operator to. Auth completes out-of-band; the source flips to `active` once the OAuth callback succeeds.

        <Warning>
          The OAuth `state` parameter Apostra attaches to the authorization URL is opaque, single-use, and carries CSRF entropy scoped to your account + source. Do **not** decode, modify, or replay it — the callback handler validates `state` server-side and rejects mismatches. If you proxy the redirect through your own server, forward `state` byte-for-byte.
        </Warning>
      </Tab>

      <Tab title="BASIC_AUTH">
        ```json theme={null}
        {
          "authenticationType": "BASIC_AUTH",
          "auth": {
            "type": "basic",
            "username": "agent-user",
            "password": "agent-password"
          }
        }
        ```

        Username and password are encoded into the outbound HTTP `Authorization: Basic ...` header when Apostra calls the source. The raw credentials are encrypted at rest and never echoed back.
      </Tab>

      <Tab title="JWT">
        ```json theme={null}
        {
          "authenticationType": "JWT",
          "auth": {
            "type": "jwt",
            "privateKey": "-----BEGIN PRIVATE KEY-----\n...",
            "issuer": "https://acme.com",
            "subject": "acme-sales",
            "keyId": "key-1",
            "scope": "agent:invoke",
            "tokenEndpointUrl": "https://auth.acme.com/oauth/token",
            "audienceUrl": "https://agent.acme-media.com/mcp"
          }
        }
        ```

        <Warning>
          JWT private keys are long-lived signing credentials. Treat them like passwords — never log them, never commit them to source control, and rotate immediately if exposed. Submitted material is encrypted at rest and never echoed back.
        </Warning>
      </Tab>

      <Tab title="NO_AUTH">
        ```json theme={null}
        { "authenticationType": "NO_AUTH" }
        ```

        Public agents only. The source is created with status `ACTIVE` immediately.
      </Tab>
    </Tabs>

    ```json Response theme={null}
    {
      "sourceId": "acme-sales",
      "name": "Acme Media Sales",
      "executionType": "AGENT",
      "status": "PENDING",
      "agentId": "agent_01HX...",
      "type": "SALES",
      "endpointUrl": "https://agent.acme-media.com/mcp",
      "protocol": "MCP",
      "authenticationType": "API_KEY",
      "authConfigured": true,
      "createdAt": "2026-04-25T12:05:00.000Z",
      "updatedAt": "2026-04-25T12:05:00.000Z"
    }
    ```

    <Warning title="AAO registry checks (connect vs. readiness)">
      AAO is checked at **connect time** and by the readiness projection, but the only hard AAO gate is connect-time **registration**. Connect-time requires that the agent is **registered** with AAO; failing or pending agents can still be connected. In readiness, AAO compliance is **informational** — a non-`passing` verdict is surfaced as a prominent warning but does not block transactions.

      | AAO status       | Connect a source                                                 | Transaction readiness                |
      | ---------------- | ---------------------------------------------------------------- | ------------------------------------ |
      | `passing`        | OK                                                               | OK                                   |
      | `pending`        | OK (logged)                                                      | OK — surfaced as an advisory warning |
      | `not-passing`    | OK (logged)                                                      | OK — surfaced as an advisory warning |
      | `not-registered` | **Rejected** with `VALIDATION_ERROR`                             | n/a                                  |
      | AAO unreachable  | **Rejected** with `SERVICE_UNAVAILABLE` — retry once it recovers | n/a                                  |

      The source still must be operational — the compatibility-named `agent_status` readiness check (see Step 4) reads the inventory source's canonical lifecycle and is a blocker for third-party sources. The connection sidecar and legacy `adcp_agent.status` value are not separate storefront lifecycle signals. AAO compliance (the `agent_connectivity` readiness check) is advisory: a non-`passing` verdict surfaces for review and does not leave the storefront `blocked`. Determining who may sell which publisher inventory stays a per-publisher authorization question against the publisher's own `adagents.json`, not this compliance score. The connect-time AAO registry gate runs uniformly for every caller — there is no SuperAdmin bypass.
    </Warning>

    Seller Accounts can connect as many external `AGENT` inventory sources as needed — no per-plan cap is enforced today. Ad-server-backed inventory sources (`MANAGED_SALES_AGENT`) are slot-exempt regardless of plan.

    Updates are partial — `PUT /api/v2/storefront/inventory-sources/:sourceId` accepts any subset of `name`, `description`, `endpointUrl`, `protocol`, `authenticationType`, `auth`, `status`. Updating `auth` rotates the stored credential; omitting it preserves the existing one.

    #### 3. How buyers discover this storefront

    Once a Storefront is transacting, buyer discovery can surface its Merchandising Agent as a first-class ADCP sales agent with ID `storefront-{platform_id}`, where `platform_id` is the Storefront's public `platformId` slug. Buyers call the Storefront surface; they do not target inventory source IDs directly in buyer discovery.

    `get_products` behavior depends on the storefront mode:

    * **Composition** — the Merchandising Agent composes buyer-facing products from active ingredient sources plus the active operating instructions.
    * **Passthrough** — the Merchandising Agent proxies `get_products` to an active source and returns the upstream products with Storefront identity overlaid.

    Matching buyer instructions are resolved at `get_products` time using operator domain, brand domain, and optional country. Storefront-built storefronts apply them during composition; Agent-supplied storefronts apply them as response overlays, including discounts and notes. Without at least one active source, buyer `get_products` has nothing to compose from or proxy to.

    <Note>
      Execution-specific configuration (per integration knobs — e.g. ad-server adapter settings for Apostra-managed ad-server sources) lives in typed fields per integration type, not in a generic config bag. New integration types add their own typed fields rather than overloading a polymorphic blob.
    </Note>

    <Note title="Ad-server sources — supported adapters">
      Ad-server-backed inventory sources (`executionType: "MANAGED_SALES_AGENT"`) wire your Storefront to one of four operator-owned ad-server adapters. Apostra manages the AdCP plumbing behind your ad server. Pick one when creating the connection via `POST /api/v2/storefront/esa`:

      | Adapter                                     | Credentials you supply                                                                                                            | How Apostra handles them                                                                                                                                                                                                                                |
      | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
      | **Google Ad Manager** (`google_ad_manager`) | Numeric network code.                                                                                                             | Apostra provisions a service account dedicated to your account; you grant it access in your GAM admin console.                                                                                                                                          |
      | **SpringServe** (`springserve`)             | Login email + password, or a pre-minted API token. New setup pins `springserve:v1` with the matching typed authentication method. | Forwarded to the managed ad-server source and encrypted there; only non-secret display configuration and the versioned contract selection stay on Apostra connection row. The source mints a fresh 2-hour token from email/password and auto-refreshes. |
      | **FreeWheel** (`freewheel`)                 | Publisher API client ID + client secret. A 7-day temporary access key is also accepted for advanced testing.                      | Forwarded to the managed ad-server source and encrypted there; never written to Apostra connection row. The client ID/secret path mints and auto-refreshes short-lived tokens.                                                                          |
      | **AdsWizz** (`adswizz`)                     | Static API key, numeric agency id, and three-letter agency billing currency.                                                      | New setup pins `adswizz:v1` with fixed Domain/Forecasting endpoints and `x-api-key` authentication. The key is encrypted by the managed source and never written to Apostra connection row.                                                             |

      Only non-secret display fields (login, environment, default advertiser/demand-partner id) are stored on the connection row so the UI can render "connected as ...". Rotating credentials on a live ad-server source preserves products, principals, and sync history via `PUT /api/v2/storefront/inventory-sources/{sourceId}/adapter-config`.
    </Note>

    <Note title="Adapter-routed storefronts — supported providers">
      Some storefronts route directly to a wired provider adapter rather than an
      inventory-source sales agent. Supported adapter provider values are `amazon`,
      `audiostack`, `google`, `meta`, `pinterest`, `reddit`, `snap`, `spotify`, and
      `tiktok`.

      Delegated OAuth for these adapter storefronts uses the shared adapter callback
      path:

      ```text theme={null}
      https://api.apostra.com/oauth/adapters/{provider}/callback
      ```

      Register the same path on staging when testing, for example
      `https://api.staging.apostra.com/oauth/adapters/snap/callback`.

      Reddit access tokens expire after one hour. Apostra requests permanent
      authorization and uses Reddit's refresh token to renew access automatically. If
      an existing Reddit connection reports expired credentials after one hour,
      reconnect it once in **Settings → Connections**; new and reconnected grants
      then refresh automatically.
    </Note>

    <Tip title="Credential handoff and testing">
      For FreeWheel and SpringServe, prefer the storefront credential screen for
      passwords and tokens rather than pasting secrets into chat. Murph can send the
      operator directly to the secure form with
      `/{accountId}/storefront?tab=sources&connectAdServer=freewheel` or
      `/{accountId}/storefront?tab=sources&connectAdServer=springserve`. That link
      opens Inventory sources, launches Connect ad server, and preselects the right
      adapter. Murph can then wait for submission, list the ad-server sources to
      find the new or updated connection, and run
      `POST /api/v2/storefront/esa/{esaId}/test-connection` to verify that the
      upstream source can authenticate.

      For testing, use a temporary API token when the ad server supports one. For
      production, the credential pair is usually better because the upstream source can
      mint and refresh short-lived tokens automatically. In either case, Apostra stores
      only non-secret display fields; the upstream source holds the encrypted
      secret.
    </Tip>

    ### Uploading setup documents to Murph

    Murph can use uploaded PDFs, decks, spreadsheets, images, and text documents
    during storefront setup. Use this for brand books, media kits, operating instructions,
    rate cards, do-not-air lists, and other materials that would otherwise need to
    be pasted into chat.

    Uploaded documents are summarized instead of copied back verbatim. The
    document-processing status includes:

    | Field                    | Type           | Description                                                                                                                                                                                                                |
    | ------------------------ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `extractedFacts`         | array          | Structured facts extracted from the uploaded files. Each fact includes `type`, `content`, `confidence`, `entities`, and `tags` so Murph can reference the source material in later turns without re-reading the full file. |
    | `brandManifestCandidate` | object \| null | Present when a brand book or visual identity document includes fields that may map into AAO `brand.json`. Includes `status`, `confidence`, `rationale`, `mappedFields`, `missingInputs`, and `recommendedActions`.         |

    For brand books, Murph can identify brand.json candidates such as name,
    website URL, colors, fonts, tone, tagline, contact details, and disclaimers.
    Logo images can be uploaded to AAO for review; pending uploads are not public
    and are not written into `brand.json` until AAO approves and lists the
    `/assets/brands/...` URL. Other assets still need public hosted URLs before
    they can be used in `brand.json`.

    <Note>
      Murph can draft and preview `brand.json` fields from an uploaded brand book,
      then compare those fields against the current AAO brand.json state and publish
      the confirmed manifest to AAO for your verified storefront operator domain.
      Uploaded logo images can be sent to AAO review from Murph; only approved AAO
      asset URLs or other public HTTPS URLs are written as logo or asset entries.
    </Note>

    #### Google Ad Manager service-account grant

    Google Ad Manager does not require the publisher to paste a password or API
    token into Apostra. Apostra creates a service account dedicated to your account
    and returns its email address from `POST /api/v2/storefront/esa/service-account`. The
    publisher grants that service-account email access inside their GAM network,
    then Apostra provisions the ad-server-backed source with the publisher's numeric
    network code.

    The operator-owned part of the flow is:

    1. Call `POST /api/v2/storefront/esa/service-account` and copy the returned
       `serviceAccountEmail`.
    2. In Google Ad Manager, go to **Admin → Global settings → Network settings →
       Add a service account user**.
    3. Enter the service-account email returned by Apostra.
    4. Grant a role that can read inventory and traffic campaigns, such as
       `Trafficker` or a least-privilege custom role with equivalent API
       permissions.
    5. Wait a few minutes for the grant to propagate.
    6. Create the ad-server source with `POST /api/v2/storefront/esa` and body
       `{ "type": "google_ad_manager", "networkCode": "12345678" }`.

    If the probe returns `ADAPTER_PERMISSION_DENIED`, verify that the exact
    service-account email was added and wait a minute or two before retrying. If it
    returns `ADAPTER_NETWORK_NOT_FOUND`, the network code is likely wrong.

    #### GAM buyer-routing default advertiser

    For Google Ad Manager managed-sales-agent sources, Storefront can clear the
    `Default GAM advertiser` setup blocker through the API. List cached advertiser
    records with `GET /api/v2/storefront/esa/{esaId}/gam/advertisers`, or create
    or find the intended catch-all advertiser with
    `POST /api/v2/storefront/esa/{esaId}/gam/advertisers/ensure`. Then set the
    tenant default with `PUT /api/v2/storefront/esa/{esaId}/gam/default-advertiser`
    using the returned `advertiser.id`.

    This flow configures the upstream sales-agent tenant directly. Operators do not
    need to open the embedded sales-agent UI to set the default GAM advertiser.

    Keep detailed GAM UI wording anchored to Google's own support documentation;
    Apostra docs should describe the contract we own, the service-account email we
    return, and the role/permission requirements we need.

    <Note>
      Sources can't be deleted while their backing agent has non-terminal media buys (`ACTIVE`, `PAUSED`, `PENDING_APPROVAL`, or `INPUT_REQUIRED`). Cancel or terminate those first.
    </Note>
  </Step>

  <Step title="Set up billing (conditional)">
    Payout details on file let Apostra settle payments on your behalf: Apostra collects from the buyer, deducts the configured fees, and pays you by bank transfer in your payout currency. Whether billing is required depends on the Seller Account execution path:

    * **Optional** only for an official Apostra sales-adapter Seller Account on an existing downstream platform settlement agreement. A third-party sales agent or Agent-supplied (finished-product) source does not qualify.
    * **Required to get paid** for every normal Seller Account, but never required to go live. Apostra clears every normal Seller Account buy today; without payout details, funds still accrue against each booking, but Apostra has no way to disburse them. The readiness check `billing_setup` returns `isBlocker: false` in every state — it is advisory, not a go-live gate. An active source that explicitly lacks `agent` billing support still blocks readiness (a different check, `interchange_billing_support`).

    <Warning>
      If billing is **optional** and you skip it, only official sales-adapter buys on the existing downstream platform agreement can operate. Apostra does not clear those media payments. Seller-cleared settlement is not yet configurable for normal Seller Accounts.
    </Warning>

    Accounts under an organization inherit billing from the organization by default. A child-account administrator cannot set up or change a payout destination; a parent administrator can create a child-specific destination for a direct Seller Account.

    #### 1. Save payout details

    Enter your bank details in **Plan & Billing → Payouts**, or via the API:

    <CodeGroup>
      ```bash curl theme={null}
      curl -X PUT https://api.apostra.com/api/v2/storefront/billing/payout-details \
        -H "Authorization: Bearer scope3_..." \
        -H "Content-Type: application/json" \
        -d '{
          "beneficiaryName": "Meridian Media Group Inc.",
          "addressLine1": "500 Harbor Blvd",
          "city": "Seattle",
          "region": "WA",
          "postalCode": "98101",
          "countryCode": "US",
          "accountNumber": "000123456789",
          "bankIdentifierType": "FEDWIRE_ABA",
          "bankIdentifierValue": "021000021",
          "currency": "USD"
        }'
      ```
    </CodeGroup>

    `accountNumber` takes a bank account number or an IBAN; `bankIdentifierType` is one of `FEDWIRE_ABA`, `CHIPS_ABA`, `SWIFT_BIC`, or `BANK_CODE`, with the identifier itself in `bankIdentifierValue`. The account number is encrypted at the application layer before storage and used only to execute payouts; the account number is write-only and never displayed after save. See [Set payout details](/v2/storefront/billing/tasks/set-payout-details) for the full contract.

    #### 2. Confirm what's on file

    <CodeGroup>
      ```bash curl theme={null}
      curl https://api.apostra.com/api/v2/storefront/billing \
        -H "Authorization: Bearer scope3_..."
      ```
    </CodeGroup>

    ```json Response theme={null}
    {
      "billing": {
        "onboardingStatus": "complete",
        "platformFeePercent": 12.5,
        "currency": "USD",
        "defaultNetDays": 30,
        "payoutDetails": {
          "beneficiaryName": "Meridian Media Group Inc.",
          "accountNumberLast4": "6789",
          "bankIdentifierType": "FEDWIRE_ABA",
          "bankIdentifierValue": "021000021",
          "completedAt": "2026-07-01T09:30:00Z"
        },
        "inherited": false
      }
    }
    ```

    #### Other billing endpoints

    | Endpoint                                  | Purpose                                                       |
    | ----------------------------------------- | ------------------------------------------------------------- |
    | `GET /api/v2/storefront/billing`          | Fees, currency, net days, masked payout details               |
    | `PUT /api/v2/storefront/billing`          | Admin-only — update fee config                                |
    | `GET /api/v2/storefront/billing/accounts` | Organizations — billing status across the accounts under them |

    <Note>
      Organization admins can pass `?targetCustomerId=<accountId>` on billing endpoints to operate on a direct Seller Account's billing. Access is validated against the organization/account relationship before each call.
    </Note>

    <Note title="Previously connected Stripe?">
      Apostra no longer uses Stripe for Seller Account payouts. Enter your bank details once (above) to keep Apostra-cleared settlement on your media buys — details held by Stripe cannot be migrated.
    </Note>
  </Step>

  <Step title="Go live">
    A storefront has no stored `PENDING` or `ACTIVE` lifecycle state. Its effective
    status is always derived from `isPaused`, archive state, and the current
    readiness checks. Buyer agents can transact only when that projection is
    `live`. Public marketplace discovery has one additional human-review gate:
    transaction-ready Storefronts remain pending marketplace review until an Apostra
    admin lists them.

    #### 1. Confirm readiness

    `GET /api/v2/storefront/readiness` computes every current requirement and the effective status. Call it any time during onboarding or operation to see what's missing.

    <CodeGroup>
      ```bash curl theme={null}
      curl https://api.apostra.com/api/v2/storefront/readiness \
        -H "Authorization: Bearer scope3_..."
      ```
    </CodeGroup>

    ```json Response theme={null}
    {
      "platformId": "acme-media",
      "status": "ready",
      "checks": [
        {
          "id": "inventory_sources",
          "name": "Inventory sources",
          "category": "inventory",
          "description": "At least one source has been configured.",
          "status": "complete",
          "isBlocker": true,
          "method": "agent",
          "details": "1 source configured"
        },
        {
          "id": "agent_status",
          "name": "Inventory source status",
          "category": "agents",
          "description": "All inventory sources are active.",
          "status": "complete",
          "isBlocker": true,
          "details": "1 of 1 inventory sources are active"
        },
        {
          "id": "agent_auth",
          "name": "Agent authentication",
          "category": "agents",
          "description": "All agents have been authenticated.",
          "status": "complete",
          "isBlocker": true,
          "details": "1 of 1 agents are authenticated"
        },
        {
          "id": "billing_setup",
          "name": "Billing",
          "category": "billing",
          "description": "Payout details are on file.",
          "status": "complete",
          "isBlocker": false,
          "details": "Payout details added"
        }
      ]
    }
    ```

    Every check also carries a `requirement` classification so you always know what a checklist item actually demands of you:

    * **`hard`** — must be resolved before the storefront can go live. Matches `isBlocker: true`.
    * **`soft`** — advisory: improves outcomes but never blocks.
    * **`platform_default`** — the platform applied a sensible default on your behalf; the `appliedDefault` field states the value in plain terms and how to change it. These are visibility items, never tasks.

    The classification is path-aware: which checks appear, and whether each blocks,
    depends on source treatment and the capability needed for that action. A
    commercial plan choice does not hide setup checks or unrelated surfaces.

    <Accordion title="What each check means">
      * **`publish_validation`** *(blocker; compatibility id)* — every active transaction path needs proof. A third-party Sales Agent Source reuses the canonical `media_buy_transaction` assertion from that Agent's exact current production revision; credentials, account mapping, reachability, and health remain Source-specific. In **Required to go live**, choose **Run transaction validation** to open the Test section for the exact Agent and inventory source that still needs proof. If an active Agent source is not yet linked to a registered Agent, Apostra opens **Source diagnostics** for that source so you can create or attach the Agent before retrying. If multiple active Agent connections match, select the connection to use or remove or reconcile the extra connections before retrying. You can also run the public transaction validation skill on that Source or ask the Agent owner to provide proof. A seller-owned no-spend sandbox test or successful live buyer media buy can still cover a Storefront-local path. An already-activated Source keeps its historical onboarding completion while current implementation health, Source health, and quarantine govern ongoing operation. Managed-only Storefronts report `complete` because Apostra operates that path.
      * **`publisher_domains`** *(blocker)* — every storefront declares at least one publisher domain so buyers know what inventory is being sold. The operator domain identifies the company operating the storefront and may be different. `adagents.json` authorization is shown separately as the advisory `publisher_authorization` check and never blocks transactions.
      * **`product_publisher_domains`** *(advisory during rollout)* — every active product should map to one of the storefront's declared publisher domains. Missing mappings and undeclared domains warn while older catalogs are backfilled; `adagents.json` authorization is separate and also advisory.
      * **`approval_settings`** *(platform default)* — how buyer submissions are handled on Apostra-managed sources. Never a task; `appliedDefault` names the current posture and how to change it.
      * **`inventory_sources`** *(blocker)* — at least one source must be connected. Buyer-facing `get_products` still requires at least one **active** source: Storefront-built storefronts need active ingredient sources, and Agent-supplied storefronts need an active source to proxy.
      * **`agent_status`** *(blocker; compatibility id)* — every non-disabled external-agent inventory source must have canonical source status `ACTIVE`. No copied sidecar or legacy agent status can independently block it. This gates go-live; AAO compliance does **not** (see `agent_connectivity`).
      * **`agent_auth`** *(blocker)* — non-OAuth agents must have a stored credential. OAuth agents are excluded once their token is captured.
      * **`agent_connectivity`** *(informational, surfaced on `GET /readiness/compliance`)* — reads each agent's AAO compliance verdict and returns per-agent track results and observations. A non-`passing` verdict surfaces as a prominent warning but does **not** block going live. See [Identity documents](/v2/concepts/identity-documents#blocks-vs-informs).
      * **`billing_setup`** *(advisory, never a go-live blocker)* — payout details are **optional** only for the official Apostra sales-adapter compatibility path; the check returns `status: optional` with `isBlocker: false` and the external-agreements warning. They are **required to get paid** for every normal Seller Account, including third-party sales-agent and Agent-supplied (finished-product) sources — but not required to go live: the check returns `status: missing` with `isBlocker: false` until payout details are on file, and the Seller Account can activate and transact in the meantime. Funds accrue against every Apostra-cleared booking either way; Apostra just can't disburse them until payout details are added. This projection is the same in non-production, so an account without payout details is never shown as billing-ready.
    </Accordion>

    Top-level `status` is `blocked` if any check with `isBlocker: true` is not `complete`, otherwise `ready`. A check with `status: optional` is treated as not required.

    #### Programmatic Agent validation

    Use the exact Agent returned by V3 `get({ kind: "agent" })` and follow its
    versioned `validationSkill`, or fetch the always-current skill directly from
    its stable alias:

    ```text theme={null}
    https://api.interchange.io/skills/test-sales-agent/SKILL.md
    ```

    Choose an available profile. The transaction profile creates an
    owned-inventory Advertiser with `sandbox: true`, resolves the authenticated
    Media Company's own Seller, and uses the ordinary V3 Advertiser, Campaign,
    Creative, Proposal, MediaBuy, and delivery tools. It may prepare temporary
    no-spend sandbox resources, then stops at a server-issued confirmation before
    staging a media buy and again before activating the sandbox campaign. Review
    each no-spend action and confirm it from the exact Agent page handoff to
    continue the same run. Do not reuse an expired, cancelled, or consumed
    confirmation; start a new run instead. Profiles shown as unavailable cannot be
    started until they have an executable fixture and confirmation path. Do not
    substitute Murph test wrappers, V2 `api_call`, or the retired standalone Test
    Runs surface.

    The brief-only profile uses public `save_connection` to enable the exact Seller
    for its fresh sandbox Advertiser before discovery. Its reverse cleanup restores
    that advertiser preference to `DEFAULT` before archiving the Advertiser; it
    does not change account-level Seller selection.

    After execution or a stop, read
    `get({ kind: "agent", include: ["validationRuns", "diagnostics"] })` and the
    exact Source diagnostics when Products were Source-attributed. A validation
    run is evidence for the Agent revision it names and records its cleanup result;
    it is not itself a certification claim.

    For a deeper agent connectivity test (full AdCP compliance scenarios in sandbox mode), hit:

    <CodeGroup>
      ```bash curl theme={null}
      curl https://api.apostra.com/api/v2/storefront/readiness/compliance \
        -H "Authorization: Bearer scope3_..."
      ```
    </CodeGroup>

    This may take up to 60 seconds.

    #### 2. Go live or resume buyer intake

    When every hard requirement is complete and the intake hold is still set, the
    Seller Setup page shows **Go live** (or **Resume new business** for a storefront
    that was live before). The action clears only the canonical intake hold and
    then reloads readiness. The page does not present the storefront as live until
    that write succeeds and the refreshed `canTransact` projection is true.

    <CodeGroup>
      ```bash curl theme={null}
      curl -X PUT https://api.apostra.com/api/v2/storefront \
        -H "Authorization: Bearer scope3_..." \
        -H "Content-Type: application/json" \
        -d '{ "isPaused": false }'
      ```
    </CodeGroup>

    Clearing the canonical `isPaused` intake hold does not claim the storefront is ready. The write succeeds, and the response still projects `blocked` with the current failing checks until every hard requirement passes. The intake hold controls buyer discovery, new buys, and buyer edits; it does not pause or resume campaigns already delivering in your ad server.

    ```json Response theme={null}
    {
      "isPaused": false,
      "canTransact": false,
      "effectiveStatus": "blocked"
    }
    ```

    When the last hard requirement becomes complete, the same storefront without an intake hold projects `live` automatically. If a future requirement is added or current evidence fails, it projects `blocked` automatically without rewriting `isPaused` or an `adcp_agent` row.

    #### 3. Marketplace review

    The marketplace review state is independent from the derived storefront lifecycle:

    | Marketplace state | Meaning                                                                                                                                   |
    | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
    | `PENDING_REVIEW`  | Default for new Storefronts. The owning organization can configure and use the Storefront, but it is not shown in public buyer discovery. |
    | `LISTED`          | Reviewed and visible in public buyer discovery and marketplace browsing, subject to the normal live/source/credential gates.              |
    | `HIDDEN`          | Intentionally removed from public buyer discovery, commonly for test, internal, or deprecated Storefronts.                                |

    Apostra reviews newly live Storefronts before listing them. This keeps test
    Storefronts and unreviewed sellers out of the broader marketplace without
    blocking the operator's own setup work.

    #### Lifecycle states

    | Status     | Meaning                                                                                                  |
    | ---------- | -------------------------------------------------------------------------------------------------------- |
    | `PENDING`  | Created but not live. Buyer-side discovery does not surface it.                                          |
    | `ACTIVE`   | Live. Buyer agents can transact; public discovery also requires marketplace review and listing.          |
    | `DISABLED` | Temporarily off. Existing media buys retain their current state and route, but new ones can't be placed. |

    Allowed transitions: `PENDING ↔ ACTIVE ↔ DISABLED` (you can't go straight from `DISABLED` to `PENDING`).
  </Step>
</Steps>

***

## Troubleshooting

<Accordion title="`AAO compliance pending` — agent is in registry but tests are still running">
  This is logged but not a blocker. The source is created and will auto-activate once its authentication requirements are configured. Reachability is reported independently; no action is required unless source health flips to `not-passing`.
</Accordion>

<Accordion title="`AAO compliance not passing` — endpoint returned `not-passing`">
  This is an advisory warning, not a source-creation or activation blocker. Visit [agenticadvertising.org](https://agenticadvertising.org) and check the storyboard test results for your agent URL. Resolve the failing scenarios in your agent implementation so the registry verdict improves and marketplace review has a clean signal.
</Accordion>

<Accordion title="`Agent must be registered with AAO to connect`">
  The agent's `endpointUrl` doesn't appear in the AAO registry at all. Register it through the AAO operator dashboard before retrying.
</Accordion>

<Accordion title="`Cannot activate storefront: All agents must be active to go live`">
  Your agent record is `PENDING`. Most often this means the auth credential hasn't been verified yet. Re-submit the source with a fresh `auth` block, or for OAuth agents make sure the OAuth callback completed.
</Accordion>

<Accordion title="`Cannot activate storefront: All agents must be authenticated to go live`">
  A non-OAuth agent has no stored credential. `PUT /api/v2/storefront/inventory-sources/:sourceId` with an `auth` block to set one.
</Accordion>

<Accordion title="`agent_connectivity` failed in compliance check">
  Look at the `compliance` array on the check — each entry has per-track `failureReason`, `summary`, and `observations`. This check is informational for activation, but it is still useful debugging signal. The most common causes are auth misconfiguration, schema drift between your agent and the AdCP spec, and agent-side timeouts beyond 60s.
</Accordion>

<Accordion title="Storefront says `operatorDomainVerified: false` even though my domain matches">
  Matching the registered `customerDomain` is not enough by itself — the account domain must also be approved by active-member email ownership or Apostra admin attestation. Same-value `operatorDomain` updates preserve the current verification state; have an Apostra admin approve the account domain or change the storefront to the correct operator domain.
</Accordion>

<Accordion title="My account belongs to an organization — whose payout details apply?">
  An account inherits its organization's payout configuration. Only a parent
  organization admin can create or change a direct child Seller Account's destination.
  In **Plan & Billing → Payouts**, select the child Seller Account; through REST,
  pass `?targetCustomerId=<account-id>`. A child-only admin cannot create an
  account-specific payout destination.
</Accordion>

***

## Next steps

* [Prepare inventory source inputs](/v2/setup/publisher-onboarding-starter-kit) — when a source needs inputs supplied separately, identify where avails, products, CRM context, creative formats, properties, execution, and reporting come from; a complete external-agent setup can skip it
* [Storefront API Reference](/v2/storefront-api-reference) — full endpoint reference for storefront, billing, and inventory source endpoints
* [Authentication](/v2/authentication) — API key and OAuth flows
* [Storefront object guide](/v2/object-guides/storefront) — how buyer agents see your storefront once it's live (discovery, credentials, sources)
