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

# Discover agents

> Find the operator and publisher agents authorized for a domain

`GET /api/v2/storefront/discover-agents`

Queries the agenticadvertising.org registry for operator records and resolves
the publisher's `adagents.json` path, including fallback discovery, for a domain.
Use it to see what the AAO registry and publisher identity documents say before
you connect or list.
Results are server-side cached for 2 minutes; pass `refresh=true` to bypass the
cache. A forced refresh reads the publisher's live `adagents.json` for publisher
properties and authorization immediately and asks the registry to recrawl so its
mirror converges. The registry's `hosting.last_validated` timestamp can therefore
remain older in that same response; use `publisher.freshness.source` to see whether
the returned publisher data came from `live_origin` or the registry snapshot.

## Request

```bash curl theme={null}
curl "https://api.apostra.com/api/v2/storefront/discover-agents?domain=acme-media.example.com&refresh=true" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

## Parameters

| Param | In | Type | Required | Notes |
| - | - | - | - | - |
| `domain` | query | string | Yes | Operator domain to discover agents for. Must be a valid domain (e.g. `example.com`) |
| `refresh` | query | `"true"` \| `"false"` | No | Set to `"true"` to bypass the 2-minute server-side cache and refetch |
| `x-aao-api-key` | header | string | No | AAO session token for the calling user. When provided, the registry response is scoped to their account and includes private/draft agents. Falls back to the platform key when omitted |

## Response

```json theme={null}
{
  "domain": "acme-media.example.com",
  "operator": {
    "domain": "acme-media.example.com",
    "member": {
      "slug": "acme-media",
      "display_name": "Acme Media"
    },
    "lookup_status": "member",
    "agents": [
      {
        "url": "https://agent.acme-media.example.com/mcp",
        "name": "Acme Media Sales Agent",
        "type": "sales",
        "compliance": {},
        "storyboards": [],
        "authorized_by": []
      }
    ]
  },
  "publisher": {
    "domain": "acme-media.example.com",
    "member": {
      "slug": "acme-media",
      "display_name": "Acme Media"
    },
    "lookup_status": "member",
    "adagents_valid": true,
    "discovery_method": "direct",
    "manager_domain": null,
    "resolved_url": "https://acme-media.example.com/.well-known/adagents.json",
    "hosting": {
      "mode": "self",
      "last_validated": "2026-06-25T14:48:42.000Z"
    },
    "freshness": {
      "source": "live_origin",
      "registry_adagents_valid": true,
      "last_validated": "2026-06-25T14:48:42.000Z",
      "recrawl_requested": true
    },
    "properties": [],
    "authorized_agents": []
  }
}
```

| Field | Type | Description |
| - | - | - |
| `domain` | string | The domain that was queried |
| `operator` | object | Agents registered to this operator domain |
| `operator.domain` | string | Operator domain |
| `operator.member` | object \| null | Registry member: `{ slug, display_name, is_founding_member?, membership_tier?, membership_tier_label? }`, or `null`. When `null`, read `operator.lookup_status` to tell *why* |
| `operator.lookup_status` | enum | Why `member` is what it is: `member` (a record is present), `not_member` (looked up with full visibility, no record), `scope_limited` (looked up without your AAO key, so members-only records may be hidden), or `lookup_failed` (the registry call timed out or errored — retry with `refresh`) |
| `operator.agents[]` | array | Each agent: `{ url, name, type }` plus optional `compliance`, `storyboards[]`, `authorized_by[]` |
| `publisher` | object | Publisher record from the AAO registry or the `adagents.json` fallback |
| `publisher.domain` | string | Publisher domain |
| `publisher.member` | object \| null | Registry member `{ slug, display_name }`, or `null`. When `null`, read `publisher.lookup_status` to tell *why* |
| `publisher.lookup_status` | enum | Why `member` is what it is: `member`, `not_member` (publisher lookups have no visibility tiers, so a null member is always definitive), or `lookup_failed` (registry call timed out or errored — retry with `refresh`) |
| `publisher.adagents_valid` | boolean \| null | Whether the `adagents.json` fallback validated, or `null` when not checked |
| `publisher.discovery_method` | string | How the file was resolved — `direct` (the domain's own `/.well-known/adagents.json`), `authoritative_location` (a centrally-hosted file covering many publishers), or `ads_txt_managerdomain` (the domain hosts no file of its own and delegates via its ads.txt `managerdomain`). Absent when no file resolved |
| `publisher.manager_domain` | string \| null | The manager domain the file was resolved from when `discovery_method` is `ads_txt_managerdomain` (e.g. `cafemedia.com`); `null` otherwise |
| `publisher.resolved_url` | string \| null | The URL the authoritative `adagents.json` was actually fetched from |
| `publisher.hosting.mode` | string | The registry's hosting state for the publisher record: `self`, `self_invalid`, `aao_hosted`, `self_redirected`, or `none`. This is registry metadata, not the same thing as `discovery_method` |
| `publisher.hosting.last_validated` | string \| null | When the registry last successfully validated the publisher-origin file. `null` means it has not successfully validated that origin yet |
| `publisher.freshness.source` | `"registry"` \| `"live_origin"` | Which source supplied the returned publisher authorization and identity. A forced refresh with a valid live file uses `live_origin` even while the registry mirror is still catching up |
| `publisher.freshness.registry_adagents_valid` | boolean \| null | The registry snapshot's own validity verdict at read time |
| `publisher.freshness.last_validated` | string \| null | The registry snapshot's last validation time; this is not the time of the live read |
| `publisher.freshness.recrawl_requested` | boolean | Present as `true` when this read asked the registry to recrawl the publisher domain |
| `publisher.properties[]` | array | Publisher property records |
| `publisher.authorized_agents[]` | array | Agents authorized to sell this publisher's inventory |

## How adagents.json resolves

`adagents.json` is the file a publisher publishes to declare which agents are
authorized to sell its inventory. Discovery does **not** stop at a `404` on the
publisher's own domain — a missing file there is normal for publishers managed by
a network. Resolution follows this order:

1. **Direct** — fetch `https://<domain>/.well-known/adagents.json`. Redirects are
   followed only within the same registrable domain (an SSRF guard: a third-party
   host can't bounce the request onto an internal address).
2. **Authoritative-location redirect** — if the direct file is a *pointer* (it sets
   `authoritative_location` and lists no `authorized_agents` of its own), follow that
   one hop to the centrally-hosted file it names. This is how a network or SSP can
   serve a single `adagents.json` for many publishers without each one hosting a full
   file. When this path is used, the response reports
   `discovery_method: "authoritative_location"`.
3. **Manager domain fallback** — if there is no direct file at all, read the domain's
   `ads.txt` and follow its `managerdomain` declaration one hop, fetching that
   manager's `adagents.json` instead. This is what makes network-managed publishers
   resolve at all: e.g. every Raptive/CafeMedia-managed domain hosts no file of its
   own and delegates to `cafemedia.com`. When this path is used, the response reports
   `discovery_method: "ads_txt_managerdomain"` and the `manager_domain` it resolved from.
4. **Authorization is tied to the publisher domain.** An agent is authorized only
   when the resolved file grants it a property that belongs to the queried domain —
   not by mere presence in `authorized_agents[]`. A centrally-hosted file covering
   many publishers cannot make an agent authorized for a domain it wasn't granted.

## Hosting mode vs. resolved file

`publisher.hosting.mode` describes the AAO registry's current publisher-record
state. It is useful for diagnosing setup, but it is not a replacement for the
resolved `adagents.json` verdict.

| Mode | Meaning |
| - | - |
| `self` | The registry successfully validated a publisher-origin file at `https://<domain>/.well-known/adagents.json` |
| `self_invalid` | The publisher origin returned a file, but it failed validation |
| `aao_hosted` | AAO hosts a publisher document for this domain. The publisher still needs to make its own origin prove that document is authoritative, typically by serving an `authoritative_location` stub at `https://<domain>/.well-known/adagents.json` pointing to the AAO-hosted URL |
| `self_redirected` | The publisher origin points to another HTTPS location through `authoritative_location` or an HTTP-layer redirect |
| `none` | The registry has no publisher-hosted or AAO-hosted file configured yet |

When a publisher moves from AAO-hosted to self-hosted, the registry only converges
after the queried domain's well-known URL is crawler-accessible. A file on a sibling
host such as `www.<domain>` is not enough by itself; `https://<domain>/.well-known/adagents.json`
must return the file or a crawler-accessible redirect that the resolver can follow.
Browser-only access, bot-gated responses, or `HEAD`/`GET` differences can leave the
registry in an unvalidated state even when a human can view a JSON file in a browser.

For AAO-hosted records, `aao_hosted` does not mean the publisher has self-hosted a
full file. It means AAO has a hosted publisher document. Origin attestation happens
when the publisher origin points at that hosted document and the registry can verify
the pointer. Until then, buyers may see AAO-hosted intent without
`adagents_json` origin verification.

For source families that sell publisher inventory through an agent URL, choose
the authorized agent URL from the storefront source model:

* If the seller connects an external AdCP sales agent, publishers authorize that
  agent's own URL.
* If the seller connects its ad server directly to Apostra and uses
  Apostra's hosted storefront agent, publishers authorize the canonical
  Apostra storefront agent URL: `https://interchange.io`.

Linked storefront inventory uses the upstream storefront's own authorization
model. Modular sources only need publisher `adagents.json` authorization when the
module or execution path exposes a publisher-facing agent URL.

For network-managed publishers, this authorization usually belongs in the
manager's file, with publishers delegating to that file through `managerdomain` or
`authoritative_location`. If one publisher has a direct `adagents.json` that points
somewhere else, that direct file can shadow the manager-domain fallback. Fix the
conflict by making the publisher's file delegate to the same manager file, or by
removing the publisher file so its `ads.txt` `managerdomain` delegation is used.

<Note>
  **AdCP 3.1 resolution semantics.** The spec extends `adagents.json` for managed
  networks: a manager publishing a large agent network can use the compact
  `publisher_domains[]` form (the authoritative file caps at 20 MB), and revocation is
  expressed via a time-limited `revoked_publisher_domains[]` — a published, dated fact
  rather than a silent removal. Apostra resolves all three discovery paths above
  (direct, `authoritative_location`, and `managerdomain` fallback); the compact
  `publisher_domains[]` / time-limited `revoked_publisher_domains[]` *content* forms are a
  tracked follow-up.
</Note>

## Errors

* `400 VALIDATION_ERROR` — `domain` missing or empty.
* `401 UNAUTHORIZED` — missing or invalid bearer token.

See [Errors](/v2/reference/errors) for the full error contract.

## Related

<CardGroup cols={2}>
  <Card title="Storefront tasks" href="/v2/storefront/tasks" icon="list-check">
    All core storefront operations.
  </Card>

  <Card title="Resolve brand" href="/v2/storefront/tasks/resolve-brand" icon="building">
    Look up a brand profile by domain.
  </Card>

  <Card title="Storefront overview" href="/v2/storefront/overview" icon="store">
    Marketplace review and AAO signals.
  </Card>
</CardGroup>


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