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

# Registry API

> Public REST API for brand resolution, property lookup, agent discovery, and authorization in the AdCP ecosystem.

The AgenticAdvertising.org registry provides a public REST API for resolving brands and properties, discovering agents, and validating authorization in the AdCP ecosystem.

## Base URL

```
https://agenticadvertising.org
```

Most endpoints are **public and require no authentication**. [Authenticated endpoints](#authenticated-endpoints) require a Bearer token.

The full [OpenAPI 3.1 specification](https://agenticadvertising.org/openapi/registry.yaml) is available for code generation and tooling. It is also discoverable at `/.well-known/openapi.yaml`.

## Quick Start

Resolve a brand domain to its canonical identity:

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://agenticadvertising.org/api/brands/resolve?domain=acmecorp.com"
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    "https://agenticadvertising.org/api/brands/resolve?domain=acmecorp.com"
  );
  const brand = await res.json();
  console.log(brand.brand_name, brand.canonical_domain);
  ```

  ```python Python theme={null}
  import requests

  brand = requests.get(
      "https://agenticadvertising.org/api/brands/resolve",
      params={"domain": "acmecorp.com"}
  ).json()
  print(brand["brand_name"], brand["canonical_domain"])
  ```
</CodeGroup>

```json Response theme={null}
{
  "canonical_id": "acmecorp.com",
  "canonical_domain": "acmecorp.com",
  "brand_name": "Acme Corp",
  "keller_type": "master",
  "house_domain": "acmecorp.com",
  "source": "brand_json"
}
```

## Rate Limits

| Endpoint                                                      | Limit                                                                                         |
| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| Brand bulk resolve (`/api/brands/resolve/bulk`)               | 20 requests and 100 unique domain resolutions/minute per IP; 25 domains and 16 KB per request |
| Property bulk resolve (`/api/properties/resolve/bulk`)        | 20 requests/minute per IP                                                                     |
| Brand reads (`/api/brands/resolve`, `/api/brands/brand-json`) | 60 requests/minute per IP                                                                     |
| Save endpoints (`/api/brands/save`, `/api/properties/save`)   | 60 requests/hour per user                                                                     |
| Crawl request (`/api/registry/crawl-request`)                 | 5 minutes per domain, 30 requests/hour per user                                               |
| All other endpoints                                           | No limit                                                                                      |

Rate-limited endpoints return `429 Too Many Requests` when the limit is exceeded. Brand bulk resolve returns `503` with a `Retry-After` header when too much resolution work is already queued.

## Endpoint Groups

<CardGroup cols={2}>
  <Card title="Brand Resolution" icon="fingerprint" href="/dist/docs/3.2.0-beta.0/registry/index#brand-resolution">
    Resolve domains to canonical brand identities, fetch brand.json files, and browse the brand registry.
  </Card>

  <Card title="Property Resolution" icon="building" href="/dist/docs/3.2.0-beta.0/registry/index#property-resolution">
    Resolve publisher domains to property information, validate adagents.json, and browse properties.
  </Card>

  <Card title="Agent Discovery" icon="robot" href="/dist/docs/3.2.0-beta.0/registry/index#agent-discovery">
    List, search, and filter agents by inventory profile. Browse publishers and view registry statistics.
  </Card>

  <Card title="Change Feed" icon="clock-rotate-left" href="/dist/docs/3.2.0-beta.0/registry/index#change-feed">
    Poll a cursor-based feed of registry changes for local sync.
  </Card>

  <Card title="Lookups & Authorization" icon="shield-check" href="/dist/docs/3.2.0-beta.0/registry/index#lookups--authorization">
    Look up agents by domain, validate product authorization, and check property authorization in real time.
  </Card>
</CardGroup>

## Lookups by entity

Three endpoints answer different questions about who is in the registry. They span two tag groups in the API reference, so use this table to pick the right lookup surface before diving into endpoint-specific parameters.

| Endpoint                               | Auth surface                                                                                                                                        | What it returns                                                                                                                                                                                        |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `GET /api/registry/agents`             | Public catalog; authenticated AgenticAdvertising.org API-tier members also see `members_only` agents.                                               | The AgenticAdvertising.org-attested member-enrolled agent catalog, with optional filters and enrichment for health, capabilities, properties, compliance, measurement metrics, and verification state. |
| `GET /api/registry/operator?domain=X`  | Auth-aware: anonymous callers see `public`; AgenticAdvertising.org API-tier members also see `members_only`; profile owners can also see `private`. | The agents operated by the queried entity plus the publishers that trust those agents.                                                                                                                 |
| `GET /api/registry/publisher?domain=X` | Public and unauthenticated; AgenticAdvertising.org membership does not change the response shape.                                                   | The inventory the queried entity publishes (`properties[]`) and the agents it authorizes (`authorized_agents[]` from `adagents.json`).                                                                 |

**`GET /api/registry/agents`** — use this when you want to browse or filter the public agent population. See [Registering an agent](/dist/docs/3.2.0-beta.0/registry/registering-an-agent) for how agents end up in this catalog. Reference: [List agents endpoint](#agent-discovery) under Agent Discovery.

**`GET /api/registry/operator?domain=X`** — use this when you have one entity in hand and want its agent footprint.

**`GET /api/registry/publisher?domain=X`** — use this when you have one publisher in hand and want its inventory and delegations.

The response's `formats[]` is a lossy display projection, not the complete publisher contract. It omits custom-format schema pointers, channel applicability, and legacy projection metadata. Pass `include=placements` to receive provenance-labeled placement summaries with resolved canonical format options for placement eligibility. Consumers doing creative validation or needing omitted declaration fields must fetch the resolved raw catalog using the returned community `registry_url` or publisher `hosting.resolved_url` / `hosting.expected_url`. The registry lookup chooses the publisher-origin/community rung and reports provenance; the raw document supplies the complete declarations.

### Brand Resolution

These endpoints resolve domains to brand identities. The `source` field identifies the provenance of the selected registry record. When more than one candidate fact exists for a domain, selection is deterministic: `hosted` wins over `brand_json`, then `community`, then `enriched`. Ties among stored rows use the most recently validated record and a stable ID tie-breaker. A normal read prefers that durable stored winner on a source tie; `fresh=true` lets a successful live origin read win a tie, but never override a higher-provenance stored record. Identical normal requests therefore do not randomly change records or labels.

| Source       | Meaning                                                                    |
| ------------ | -------------------------------------------------------------------------- |
| `hosted`     | Registered by a domain owner whose control of the domain has been verified |
| `brand_json` | Resolved from the domain's `/.well-known/brand.json` file                  |
| `enriched`   | Enriched via Brandfetch API                                                |
| `community`  | Submitted by a community member                                            |

All sources produce the same resolution response structure, but they are not equivalent evidence. `hosted` and `brand_json` are backed by domain control; `community` and `enriched` describe what the registry has learned without proving that the domain operator asserted it. `source` is identity provenance, not proof of a house relationship—use `relationship_trust` for that. To get full brand identity data (logos, colors, tone), use `/api/brands/enrich` or look up the brand in the registry.

#### Reading trust

A domain is authoritative for its own identity — TLS proves the document came from that domain and nothing more. Relationships between a brand and a house need both sides to publish them, so resolution reports how each relationship was established rather than flattening it into a single answer.

For third-party verification, call `GET /api/brands/resolve?domain=<leaf-domain>` and inspect `relationship_trust`. Only `mutual` and `inline` are reciprocated relationships suitable for extending trust to `house_domain`:

* `mutual` means the leaf publishes `house_domain` and the house names the leaf in `brand_refs[]`; `relationship_verified_at` reports when the resolver last observed both sides agreeing.
* `inline` means the house's own canonical document defines the leaf, so the ownership statement and identity come from the house authority itself.

Every other value is non-reciprocated. In particular, `claimed_house_domain` with `leaf_only` is one side's self-assertion and must not authorize the leaf as part of that house. There is no separate ordered-chain endpoint in v3: the hierarchy is one level deep, and the reciprocated `house_domain` edge in this response is the supported ownership-verification result.

An absent `relationship_trust` is not the same as `standalone`: it means the selected record carries no relationship verdict. Consumers must treat missing evidence as unknown, not as evidence that no relationship exists.

| `relationship_trust` | Meaning                                                                                                                                                   |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inline`             | The house's own document defines the brand.                                                                                                               |
| `mutual`             | Both sides publish the relationship. The trust-extending edge.                                                                                            |
| `leaf_only`          | The brand claims a house whose reciprocal declaration is missing, not yet active, or older than the resolver's trust ceiling. See `claimed_house_domain`. |
| `house_only`         | A house claims the brand, which has not claimed the house.                                                                                                |
| `standalone`         | The brand claims no house.                                                                                                                                |
| `unverifiable`       | A claimed house exists but could not be checked.                                                                                                          |

`house_domain` is only set on a trust-extending relationship; a one-sided claim appears as `claimed_house_domain`. For `mutual`, `relationship_verified_at` records when both sides were last seen agreeing — it does not advance while the house side is unreachable. `relationship_declared_at` records the house's `brand_refs[].effective_at`, falling back to the resolver's durable first observation when the publisher omits it. AgenticAdvertising.org's reference resolver requires both clocks to remain live: confirmation may be retained for at most 24 hours while the house is unreachable, and declarations age out after 180 days even when re-confirmation keeps succeeding. Future declarations remain `leaf_only` until their effective time. Callers can apply stricter policies to either timestamp.

Following a redirect never lets a domain adopt a house's identity. When a domain's brand.json points at a house that does not name the domain back — in `properties[]` as `relationship: "owned"`, or in `brand_refs[]` — the response carries the claim (`claimed_house_domain` with `relationship_trust: leaf_only`) and no brand identity. A house that lists a domain with `direct`, `delegated`, or `ad_network` is declaring a monetization path, not ownership, so it does not resolve that domain's identity either. If you expected an identity here, the house is missing its half of the relationship.

`fresh=true` bypasses the resolution cache and refetches from the origin. When that fetch fails and a stored record is returned instead, `live_brand_json` carries that request's failed-fetch diagnostics so you can distinguish "stored evidence returned" from "origin checked successfully." Apply your own freshness policy to that fallback rather than silently treating it as live.

Pre-v3 documents are promoted to canonical v3 identity for the response, flagged with `promoted_from_schema` and `migration_warnings`. Promotion covers identity only; legacy relationship data is preserved opaquely and never becomes a trust claim. In particular, a warning with `field: "house"` means the legacy house object was moved to `legacy_metadata.legacy_house` and did **not** establish `house_domain`. The publisher must add `house_domain` to the leaf document and a reciprocal `brand_refs[]` entry to the house document.

#### Trust fields in brand lists

`/api/brands/registry` and `/api/brands/find` include `relationship_trust`, `relationship_verified_at`, and `claimed_house_domain` on each result row. These are cached from the last crawler resolution cycle and use the same semantics as the `relationship_trust` field on `/api/brands/resolve` responses. The cache staleness is bounded by the crawl interval.

An absent `relationship_trust` in a list result means trust has not yet been computed for that brand — it MUST NOT be interpreted as `standalone`. For an authoritative, real-time verdict use `/api/brands/resolve`.

| Method | Path                       | Description                                                  |
| ------ | -------------------------- | ------------------------------------------------------------ |
| GET    | `/api/brands/resolve`      | Resolve a domain to its canonical brand                      |
| POST   | `/api/brands/resolve/bulk` | Resolve up to 25 domains at once                             |
| GET    | `/api/brands/brand-json`   | Fetch raw brand.json for a domain                            |
| GET    | `/api/brands/registry`     | List all brands (search, pagination) with cached trust state |
| GET    | `/api/brands/enrich`       | Enrich brand data via Brandfetch                             |
| GET    | `/api/brands/history`      | Edit history for a brand                                     |
| POST   | `/api/brands/save`         | Save or update a community brand (auth required)             |

### Property Resolution

| Method | Path                           | Description                                      |
| ------ | ------------------------------ | ------------------------------------------------ |
| GET    | `/api/properties/resolve`      | Resolve a domain to its property info            |
| POST   | `/api/properties/resolve/bulk` | Resolve up to 100 domains at once                |
| GET    | `/api/properties/registry`     | List all properties (search, pagination)         |
| GET    | `/api/properties/validate`     | Validate a domain's adagents.json                |
| GET    | `/api/properties/history`      | Edit history for a property                      |
| POST   | `/api/properties/save`         | Save or update a hosted property (auth required) |

### Agent Discovery

| Method | Path                          | Description                                                     |        |             |            |          |       |        |            |
| ------ | ----------------------------- | --------------------------------------------------------------- | ------ | ----------- | ---------- | -------- | ----- | ------ | ---------- |
| GET    | `/api/registry/agents`        | List all member-enrolled agents — filter by type (\`?type=brand | rights | measurement | governance | creative | sales | buying | signals\`) |
| GET    | `/api/registry/agents/search` | Search agents by inventory profile (auth required)              |        |             |            |          |       |        |            |
| GET    | `/api/registry/publishers`    | List all publishers                                             |        |             |            |          |       |        |            |
| GET    | `/api/registry/stats`         | Registry statistics                                             |        |             |            |          |       |        |            |
| POST   | `/api/registry/crawl-request` | Request re-crawl of a publisher domain (auth required)          |        |             |            |          |       |        |            |

#### Measurement-vendor discovery

To discover measurement vendors specifically (Adelaide-style attention, Scope3-style emissions, Nielsen DAR, IAS/DV custom quality, etc.), filter the agent list by `type=measurement`:

```bash theme={null}
curl "https://agenticadvertising.org/api/registry/agents?type=measurement"
```

This returns every measurement agent enrolled in the registry by an AgenticAdvertising.org member organization.

**Per-metric catalog discovery.** Each measurement agent publishes its full per-metric catalog at [`get_adcp_capabilities.measurement.metrics[]`](/dist/docs/3.2.0-beta.0/protocol/get_adcp_capabilities#measurement) — the canonical, vendor-controlled source of truth. AgenticAdvertising.org crawls each measurement agent's `get_adcp_capabilities` on a TTL and stores the result; passing `?capabilities=true` folds the catalog into the response next to `creative_capabilities` and `signals_capabilities`.

##### Trust and external links

The registry caches measurement catalogs; it does not endorse or independently review their links or claims. Read each URL according to its field-level purpose:

| Field                           | Meaning                                                                                                                               | What it does not prove                                                                             |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `methodology_url`               | Vendor-published methodology documentation for the metric.                                                                            | That AgenticAdvertising.org, an accreditor, or another independent party reviewed the methodology. |
| `standard_reference`            | The industry-standard document the vendor says the metric implements.                                                                 | That the implementation conforms to the standard or holds accreditation.                           |
| `accreditations[].evidence_url` | The vendor-supplied pointer intended to lead to the accreditor's public listing or other public evidence for the named accreditation. | That the accreditation is valid, current, or independently verified by the registry.               |

An accreditation entry is a vendor assertion while `verified_by_aao` is `false`. Treat it as independently verified only when a separate, explicit attestation or verification state establishes who verified the claim, what was verified, and when. Enrollment, catalog caching, a plausible accreditor name, and an `evidence_url` alone do not establish that state.

Clients must treat all three fields as untrusted external-link input. Parse the value as a URL, offer clickable links only for HTTPS destinations, show the destination hostname, and use an external-site confirmation when the destination is not already trusted by the user. A new-tab link should set `target="_blank"` with `rel="noopener noreferrer"`; render escaped text and attributes rather than interpolating catalog values through `innerHTML`.

Fetching a link is a separate security decision from rendering it. Browser and server clients should not fetch these URLs merely to display a catalog. A service that deliberately retrieves methodology or evidence content must apply its normal SSRF and privacy controls, including blocking private, loopback, link-local, and metadata-service destinations and revalidating every redirect.

These semantics are stable properties of the named fields, so the response does not repeat them in a top-level `_meta` path list or per-value provenance flag. Such metadata would duplicate the schema contract without turning a vendor-supplied link into verified evidence.

**Filter parameters** (all imply `type=measurement` when present; an explicit non-measurement type returns 400):

| Param                       | Match                                                        | Notes                                                                                                                                                                                                                                                                                                                                                |
| --------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `metric_id=attention_units` | Exact match on `metrics[].metric_id`                         | Repeatable; multiple values are AND'd (vendor must carry all named metrics).                                                                                                                                                                                                                                                                         |
| `accreditation=MRC`         | Exact match on `metrics[].accreditations[].accrediting_body` | Repeatable; multiple values are AND'd (vendor must carry all named accreditations). When combined with `metric_id`, a cross-product AND applies — see combined filter semantics below. Vendor-asserted — `verified_by_aao` is always `false` in the response; renderers should mark these as vendor claims, not AgenticAdvertising.org endorsements. |
| `q=attention`               | Case-insensitive substring on `metric_id`                    | v1 scope: metric\_id only. Max 64 chars; SQL wildcards (`%`, `_`) rejected. Description/standard fuzzy search is a follow-up.                                                                                                                                                                                                                        |

**Combined filter semantics.** When both `metric_id` and `accreditation` are present, per-metric semantics apply: the same `metrics[]` element must satisfy both constraints simultaneously. A vendor with an `attention_units` metric and a separately MRC-accredited `viewability` metric does **not** match — only a vendor whose `attention_units` metric itself carries MRC accreditation does.

With multiple values, every `(metric_id, accreditation)` pair must be satisfied by at least one metrics element. This is a cross-product AND, not an OR within accreditation:

```bash theme={null}
# Vendor must have MRC-accredited attention_units AND MRC-accredited emissions
curl "https://agenticadvertising.org/api/registry/agents?metric_id=attention_units&metric_id=emissions&accreditation=MRC"
```

Using multiple `accreditation` values alongside multiple `metric_id` values produces an all-pairs AND (`M × N` constraints) and typically returns few or no results. Prefer single `accreditation` with multiple `metric_id` values for discovery.

```bash theme={null}
# All vendors offering attention measurement
curl "https://agenticadvertising.org/api/registry/agents?metric_id=attention_units&capabilities=true"

# MRC-accredited viewability vendors
curl "https://agenticadvertising.org/api/registry/agents?type=measurement&accreditation=MRC&q=viewab&capabilities=true"
```

**Direct call vs. index — when to use which.** Buyers have two paths to a vendor's metric catalog:

| Use case                                                         | Path                                   | Why                                                                                                                                                                                            |
| ---------------------------------------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Discovery / planning ("which vendors offer attention?")          | Registry index (`?metric_id=...`)      | Pre-aggregated, cached, fast. Cross-vendor in one call. Stale up to the registry TTL window (typically 24h).                                                                                   |
| Settlement / audit ("does Adelaide currently support metric X?") | Direct call to `get_adcp_capabilities` | Live, canonical, no staleness. The buyer is already calling the measurement agent at delivery / reconciliation time; one extra call is cheap and removes index staleness from the audit trail. |
| Filtering at `get_products` time                                 | Registry index                         | Buyer is in a fast-path query and the seller's product catalog already needs to know which vendors are valid.                                                                                  |

#### Creative-agent discovery

Endpoints publish their canonical build, validation, and preview surface in [`get_adcp_capabilities.creative.supported_formats[]`](/dist/docs/3.2.0-beta.0/protocol/get_adcp_capabilities). The registry snapshots those self-declarations and supports reverse lookup without calling every candidate. Results can include standalone creative agents and mixed sales/creative agents; registry `type` is the endpoint's primary role, not an exclusion of its other capability blocks.

Creative filters do not force a primary `type`, because mixed sales/creative endpoints are valid renderer candidates. Multiple `format_kind` or `creative_operation` values are OR'd within that parameter; different parameters must match the same `supported_formats[]` entry.

| Param                | Match                                     |
| -------------------- | ----------------------------------------- |
| `format_kind`        | Exact canonical `format.format_kind`      |
| `publisher_domain`   | Exact publisher namespace on `format`     |
| `format_option_id`   | Exact publisher format option on `format` |
| `capability_id`      | Exact agent-local capability identifier   |
| `creative_operation` | `build`, `validate`, or `preview`         |

```bash theme={null}
# Agents claiming they can build a fictional publisher's exact vertical-video contract
curl "https://agenticadvertising.org/api/registry/agents?publisher_domain=shorts.streamhaus.example&format_option_id=vertical_video&creative_operation=build&capabilities=true"

# Generic hosted-video builders
curl "https://agenticadvertising.org/api/registry/agents?format_kind=video_hosted&creative_operation=build"
```

These are endpoint self-claims, not publisher endorsements. Before production use, call the selected endpoint's `get_adcp_capabilities` directly to refresh the declaration and then route `build_creative` with that entry's `capability_id` when `operations` includes `build`.

### Change Feed

| Method | Path                        | Description                                                                     |
| ------ | --------------------------- | ------------------------------------------------------------------------------- |
| GET    | `/api/registry/feed`        | Poll cursor-based registry change feed (auth required)                          |
| GET    | `/api/registry/feed/stream` | Stream cursor-based registry feed pages over Server-Sent Events (auth required) |

### Lookups & Authorization

| Method | Path                                            | Description                              |
| ------ | ----------------------------------------------- | ---------------------------------------- |
| GET    | `/api/registry/lookup/domain/{domain}`          | Find agents authorized for a domain      |
| GET    | `/api/registry/lookup/property`                 | Find agents by property identifier       |
| GET    | `/api/registry/lookup/agent/{agentUrl}/domains` | Get all domains for an agent             |
| POST   | `/api/registry/validate/product-authorization`  | Validate agent product authorization     |
| POST   | `/api/registry/expand/product-identifiers`      | Expand property selectors to identifiers |
| GET    | `/api/registry/validate/property-authorization` | Real-time authorization check            |

Authorization validation checks both sides: the publisher's `adagents.json` (does it authorize this agent with the claimed `delegation_type`?) and the operator's `brand.json` (does it declare this property with a matching `relationship`?).

### Validation Tools

| Method | Path                     | Description                         |
| ------ | ------------------------ | ----------------------------------- |
| POST   | `/api/adagents/validate` | Validate adagents.json for a domain |
| POST   | `/api/adagents/create`   | Generate adagents.json content      |

### Search

| Method | Path                        | Description                                      |
| ------ | --------------------------- | ------------------------------------------------ |
| GET    | `/api/search`               | Search across brands, publishers, and properties |
| GET    | `/api/manifest-refs/lookup` | Find manifest references for a domain            |

### Agent Probing

| Method | Path                             | Description                                                             |
| ------ | -------------------------------- | ----------------------------------------------------------------------- |
| GET    | `/api/public/discover-agent`     | Probe an agent URL for capabilities                                     |
| GET    | `/api/public/agent-formats`      | Get canonical `creative.supported_formats[]` capabilities from an agent |
| GET    | `/api/public/agent-products`     | Get products from a sales agent                                         |
| GET    | `/api/public/validate-publisher` | Validate a publisher domain                                             |

## Activity history

`GET /api/brands/history?domain={domain}` and `GET /api/properties/history?domain={domain}` return the edit history for a registry entry, newest first. These are public endpoints — no authentication required.

```json Response theme={null}
{
  "domain": "acmecorp.com",
  "total": 3,
  "revisions": [
    {
      "revision_number": 3,
      "editor_name": "Pinnacle Media",
      "edit_summary": "Updated logo URL",
      "source": "community",
      "is_rollback": false,
      "created_at": "2026-03-01T12:34:56Z"
    },
    {
      "revision_number": 2,
      "editor_name": "system",
      "edit_summary": "API: enriched via Brandfetch",
      "source": "enriched",
      "is_rollback": false,
      "created_at": "2026-02-15T08:00:00Z"
    }
  ]
}
```

Entries with `editor_name: "system"` were written by automated enrichment. When `is_rollback` is `true`, `rolled_back_to` contains the revision number that was restored. Pagination uses `limit` (max 100) and `offset` query parameters.

## Anti-abuse and anti-homograph controls

Because `/api/brands/save`, `/api/properties/save`, and the `adagents` validation endpoints accept domain strings from authenticated member organizations, the hosted registry applies a layered floor of anti-abuse controls at save time. These are operational behaviors of the AgenticAdvertising.org registry in the 3.x era — not a new wire surface — and exist so that typosquats, confusable lookalikes, and drive-by brand hijacks cannot get written into the index by a single authenticated caller.

* **Domain normalization (IDNA 2008 + confusable detection).** Save endpoints SHOULD apply IDNA 2008 to normalize internationalized domain names to ASCII before persistence, and SHOULD then run Unicode confusable-detection (for example, ICU `uspoof` or equivalent) against two corpora: (1) already-registered entries in the index, and (2) a curated high-value-brand deny list maintained by the registry operator. The deny list catches typosquats of well-known brands before those brands themselves are registered (e.g., a `g00gle.com` submission collides with the deny-list entry even if Google has not yet claimed an index row). Ambiguous submissions — mixed-script labels, homograph collisions, disallowed Unicode classes — SHOULD be rejected or flagged for human review rather than silently committed.
* **Ownership proof before commit.** Save endpoints MUST require evidence of domain control before a **new** brand or property entry is committed to the index — this is the threat that motivates the control, since an attacker with a compromised member API key would otherwise be free to bulk-register fresh confusable variants that have no prior entry to conflict with. For **revisions** to an existing community-source entry by the same authenticated organization, re-proof SHOULD be required on a rolling basis (for example, once the prior proof is older than 90 days) but MAY be skipped within that window. Accepted proofs are either a DNS TXT record at `_adcp-owner.{domain}` matching a server-issued nonce, or an HTTP challenge hosted at `/.well-known/adcp-ownership.txt` on the domain. Nonces MUST be single-use, scoped to the `(organization, domain)` pair, and MUST expire within **15 minutes** of issuance; verification MUST consume the nonce on success and invalidate it on failure. A leaked or unused nonce after expiry is dead. Revisions to an existing **authoritative** entry (i.e., one backed by `brand.json` / `adagents.json`) continue to follow the 409 Conflict semantics in [Save brand](#save-brand) and [Save property](#save-property); ownership proof covers the community-source save path.
* **Per-organization rate limits on saves.** In addition to the per-IP rate limits documented in [Rate Limits](#rate-limits), save endpoints SHOULD apply per-organization limits so that a single compromised API key cannot bulk-register confusable variants. The hosted implementation uses a burst-tolerant cap (indicative: tens of saves per hour per org, low-hundreds per day per org); callers exceeding the per-org bucket receive `429 Too Many Requests`.

These controls are enforced by the hosted AgenticAdvertising.org registry. Self-hosted mirrors consuming the [Change Feed](#change-feed) rely on the hosted registry's save-time checks and do not re-run them — which is consistent with the feed's advisory-identity posture (see `specs/registry-change-feed.md` §Advisory identity material): the feed is change-detection, not a trust anchor, and the publisher's own `adagents.json` pin remains the authoritative identity source (see [`adagents.json` §`signing_keys`](/dist/docs/3.2.0-beta.0/governance/property/adagents#signing_keys)). Operators running an alternative registry implementation SHOULD apply equivalent save-time controls before accepting community-source writes.

## Authentication

Public endpoints (resolution, discovery, search) require no authentication. Write endpoints accept either an **organization API key** (server-to-server) or a **user JWT** obtained via OAuth 2.1 (interactive / agent clients). Both are sent in the `Authorization: Bearer ...` header.

### Option A: Organization API key

Long-lived, org-scoped. Best for server-to-server integrations where no user is present.

1. Sign in at [agenticadvertising.org/dashboard/api-keys](https://agenticadvertising.org/dashboard/api-keys)
2. Click **Create key** and copy the generated key

Pass the key in the `Authorization` header:

```
Authorization: Bearer sk_...
```

### Option B: User SSO via OAuth 2.1

Short-lived, user-scoped. Best for agent clients (MCP, AI assistants, custom apps) where a human is signing in to AAO. A single token works against both `/mcp` and the REST API.

Discovery follows [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) and [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728):

* Authorization server metadata: `GET /.well-known/oauth-authorization-server`
* Protected-resource metadata (REST API): `GET /.well-known/oauth-protected-resource/api`
* Protected-resource metadata (MCP): `GET /.well-known/oauth-protected-resource/mcp`

The flow is authorization code with PKCE. Dynamic client registration ([RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591)) is available at `/register`. Users authenticate via AuthKit; the token is a WorkOS-signed JWT.

```
Authorization: Bearer <jwt>
```

A valid user JWT proves identity, not entitlement. Endpoints gated on organization membership or admin role (most write endpoints) still return `403` if the authenticated user lacks the required standing.

Community-mirror submissions are the exception to direct role rejection: `PUT /api/registry/mirrors/{platform}` accepts any authenticated organization. A registry moderator or AgenticAdvertising.org administrator publishes immediately; other callers create a pending proposal and receive `202 Accepted` plus `Location`, `status_url`, and `proposal_digest`. Review decisions must echo that digest, and approval fails if the public mirror changed since submission. Contributor proposals are limited to 1 MiB and 2,000 catalog items. This keeps community contribution open without allowing an organization credential to publish a global fallback catalog on another platform's behalf without bounded, revision-safe review.

### Authenticated endpoints

These endpoints require a valid API key.

#### Save brand

`POST /api/brands/save`

Save or update a community brand in the registry. For existing brands, creates a revision-tracked edit. Cannot edit authoritative brands managed via `brand.json` — those return `409 Conflict`.

**Request body:**

```json theme={null}
{
  "domain": "acmecorp.com",
  "brand_name": "Acme Corp",
  "brand_manifest": {
    "name": "Acme Corp",
    "description": "A fictional company",
    "logos": [{ "url": "https://acmecorp.com/logo.svg", "tags": ["icon"] }],
    "colors": [{ "hex": "#FF5733", "type": "accent" }]
  }
}
```

`domain` and `brand_name` are required. `brand_manifest` (brand identity data) is optional. The brand's `source` is set to `"community"` by the server. Domains are normalized (protocol stripped, lowercased).

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://agenticadvertising.org/api/brands/save" \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"domain":"acmecorp.com","brand_name":"Acme Corp"}'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    "https://agenticadvertising.org/api/brands/save",
    {
      method: "POST",
      headers: {
        "Authorization": "Bearer YOUR_API_KEY",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        domain: "acmecorp.com",
        brand_name: "Acme Corp",
      }),
    }
  );
  const result = await res.json();
  ```

  ```python Python theme={null}
  import requests

  result = requests.post(
      "https://agenticadvertising.org/api/brands/save",
      headers={"Authorization": "Bearer YOUR_API_KEY"},
      json={"domain": "acmecorp.com", "brand_name": "Acme Corp"},
  ).json()
  ```
</CodeGroup>

```json Response (create) theme={null}
{
  "success": true,
  "message": "Brand \"Acme Corp\" saved to registry",
  "domain": "acmecorp.com",
  "id": "br_abc123"
}
```

```json Response (update) theme={null}
{
  "success": true,
  "message": "Brand \"Acme Corp\" updated in registry (revision 2)",
  "domain": "acmecorp.com",
  "id": "br_abc123",
  "revision_number": 2
}
```

#### Save property

`POST /api/properties/save`

Save or update a hosted property in the registry. For existing properties, creates a revision-tracked edit. Cannot edit authoritative properties managed via `adagents.json` — those return `409 Conflict`.

This is an identity-only write surface: the stored document always carries `authorized_agents: []`. Sales authorization lives solely in the publisher's own origin `adagents.json` — the community registry cannot mint or carry it — so any `authorized_agents` sent in the request body is ignored.

**Request body:**

```json theme={null}
{
  "publisher_domain": "examplepub.com",
  "properties": [
    { "type": "website", "name": "Example Publisher" }
  ],
  "contact": {
    "name": "Ad Ops",
    "email": "adops@examplepub.com"
  }
}
```

`publisher_domain` is required. `properties` (each requiring `type` and `name`) and `contact` are optional. Domains are normalized (protocol stripped, lowercased).

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://agenticadvertising.org/api/properties/save" \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "publisher_domain": "examplepub.com",
      "properties": [{"type": "website", "name": "Example Publisher"}]
    }'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    "https://agenticadvertising.org/api/properties/save",
    {
      method: "POST",
      headers: {
        "Authorization": "Bearer YOUR_API_KEY",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        publisher_domain: "examplepub.com",
        properties: [{ type: "website", name: "Example Publisher" }],
      }),
    }
  );
  const result = await res.json();
  ```

  ```python Python theme={null}
  import requests

  result = requests.post(
      "https://agenticadvertising.org/api/properties/save",
      headers={"Authorization": "Bearer YOUR_API_KEY"},
      json={
          "publisher_domain": "examplepub.com",
          "properties": [{"type": "website", "name": "Example Publisher"}],
      },
  ).json()
  ```
</CodeGroup>

```json Response (create) theme={null}
{
  "success": true,
  "message": "Hosted property created for examplepub.com",
  "id": "prop_xyz789"
}
```

```json Response (update) theme={null}
{
  "success": true,
  "message": "Property 'examplepub.com' updated (revision 2)",
  "id": "prop_xyz789",
  "revision_number": 2
}
```

#### Change feed

`GET /api/registry/feed`

Poll a cursor-based feed of registry changes. Use this to keep a local copy of the registry in sync without re-fetching the full dataset. Events are ordered by UUID v7 `event_id`, providing monotonic cursor progression. The feed retains events for 90 days — expired cursors return `410 Gone`. Each response includes `freshness` metadata so consumers can measure feed lag for the requested type filter.

**Schema:** [`core/registry-feed-response.json`](https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/registry-feed-response.json) wraps [`core/registry-event.json`](https://adcontextprotocol.org/schemas/3.2.0-beta.0/core/registry-event.json) items.

**Query parameters:**

| Parameter | Type   | Default | Description                                                                     |
| --------- | ------ | ------- | ------------------------------------------------------------------------------- |
| `cursor`  | UUID   | —       | Resume after this event ID. Omit for the earliest available events.             |
| `types`   | string | —       | Comma-separated event type filters. Supports glob patterns (e.g. `property.*`). |
| `limit`   | number | 100     | Max events per page (1–10,000).                                                 |

**Event types:**

| Type                            | Description                                                                                                                                                                                                              |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `property.created`              | A new property was added to the registry                                                                                                                                                                                 |
| `property.updated`              | Property metadata changed                                                                                                                                                                                                |
| `property.merged`               | Two property records were merged                                                                                                                                                                                         |
| `property.stale`                | Property failed re-crawl validation                                                                                                                                                                                      |
| `property.reactivated`          | A stale property passed re-crawl                                                                                                                                                                                         |
| `collection.created`            | A new publisher collection was added to the registry                                                                                                                                                                     |
| `collection.updated`            | Collection metadata or distribution identifiers changed                                                                                                                                                                  |
| `collection.merged`             | Two collection records were merged                                                                                                                                                                                       |
| `collection.removed`            | A collection is no longer visible in the publisher's authoritative catalog                                                                                                                                               |
| `agent.discovered`              | A new `agent_url` appeared in a publisher's `adagents.json` (authorization graph; does not imply the agent is in `/api/registry/agents`)                                                                                 |
| `agent.removed`                 | An agent was removed from the registry                                                                                                                                                                                   |
| `agent.profile_updated`         | Agent inventory profile changed                                                                                                                                                                                          |
| `agent.compliance_changed`      | Agent compliance or verification status changed                                                                                                                                                                          |
| `agent.verification_earned`     | Agent earned an AAO Verified badge                                                                                                                                                                                       |
| `agent.verification_lost`       | Agent lost an AAO Verified badge                                                                                                                                                                                         |
| `publisher.adagents_discovered` | A publisher's adagents.json was discovered and projected into the registry                                                                                                                                               |
| `publisher.adagents_changed`    | A semantic field of a publisher's adagents.json changed, including a formats-only or placements-only revision. The payload includes `changed_fields`, `format_count`, and `placement_count` on newly emitted 3.2 events. |
| `authorization.granted`         | An agent was authorized for a property                                                                                                                                                                                   |
| `authorization.revoked`         | An authorization was removed                                                                                                                                                                                             |
| `authorization.modified`        | An authorization stayed visible, but externally visible metadata changed (including rotation of `signing_keys`)                                                                                                          |

`authorization.*` payloads carry `signing_keys` — the publisher-pinned JWK set from `authorized_agents[*].signing_keys` in `adagents.json`. Consumers verifying inbound TMP signatures key on `kid → JWK`; null when the publisher declared no keys, in which case consumers fall back to the agent-hosted JWKS per [spec R-2](/dist/docs/3.2.0-beta.0/governance/property/adagents#signing_keys). Note that per `specs/registry-change-feed.md` §Advisory identity material the feed is change-detection, not a trust anchor: consumers MUST re-verify against the publisher's own `adagents.json` before acting on identity changes.

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://agenticadvertising.org/api/registry/feed?types=property.*&limit=50" \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    "https://agenticadvertising.org/api/registry/feed?types=property.*&limit=50",
    { headers: { Authorization: "Bearer YOUR_API_KEY" } }
  );
  const feed = await res.json();
  // Store feed.cursor for next poll
  ```

  ```python Python theme={null}
  import requests

  feed = requests.get(
      "https://agenticadvertising.org/api/registry/feed",
      headers={"Authorization": "Bearer YOUR_API_KEY"},
      params={"types": "property.*", "limit": 50},
  ).json()
  # Store feed["cursor"] for next poll
  ```
</CodeGroup>

The crawler compares every semantic top-level field of the normalized document. Changes confined to `formats[]` or `placements[]` therefore emit `publisher.adagents_changed`; changes only to transport/version metadata (`$schema` or `last_updated`) do not. Consumers may use `changed_fields` to target their refresh, but should re-resolve every field they depend on when reading an older retained event that lacks this field. **3.2 migration note:** handlers that previously reacted only to `agents_added`, `agents_removed`, `properties_added`, or `properties_removed` should also inspect `changed_fields` — a formats-only or placements-only revision emits this event with those count fields unchanged and `changed_fields` set to `["formats"]` or `["placements"]`.

```json Response theme={null}
{
  "events": [
    {
      "event_id": "019539a0-1234-7000-8000-000000000001",
      "event_type": "property.created",
      "entity_type": "property",
      "entity_id": "019539a0-b1c2-7000-8000-000000000002",
      "payload": {
        "property_rid": "019539a0-b1c2-7000-8000-000000000002",
        "classification": "property",
        "source": "contributed",
        "identifiers": [
          { "type": "domain", "value": "streamer.example.com" }
        ]
      },
      "actor": "crawler",
      "created_at": "2026-03-31T10:00:00.000Z"
    }
  ],
  "cursor": "019539a0-1234-7000-8000-000000000001",
  "has_more": true,
  "freshness": {
    "generated_at": "2026-03-31T10:00:15.000Z",
    "latest_event_created_at": "2026-03-31T10:00:00.000Z",
    "lag_seconds": 15,
    "retention_days": 90
  }
}
```

When `has_more` is `true`, pass the returned `cursor` value in the next request to continue polling. When `false`, you've reached the end of the current feed — poll again later with the same cursor to pick up new events.

`latest_event_created_at` is the newest matching event currently visible to the feed. `lag_seconds` is computed against `generated_at`; alert on it according to your own mirror freshness target.

`GET /api/registry/feed/stream` provides the same feed pages over Server-Sent Events. Reconnect with your last persisted cursor after disconnects. The stream emits `event: feed` with the same JSON shape shown above and `event: heartbeat` while caught up. Cursors are tied to the same logical `types` subscription; changing filters while reusing a cursor can skip events that were filtered out previously. The stream carries the resume cursor in JSON `data.cursor`, not SSE `id`, so browser `EventSource` clients must reconnect with `?cursor=...`.

If the cursor has expired (older than 90 days or not found), the response is `410 Gone`:

```json 410 Gone theme={null}
{
  "error": "cursor_expired",
  "message": "Cursor is older than 90-day retention window. Re-bootstrap from /registry/agents/search, /catalog, and /catalog/collections/sync."
}
```

#### Agent search

`GET /api/registry/agents/search`

Search agents by inventory profile — channels, markets, content categories, property types, and more. Filters use AND across dimensions and OR within a dimension. Results are ranked by a relevance score based on filter match breadth, inventory depth, and TMP support.

**Query parameters:**

| Parameter        | Type    | Default | Description                                              |
| ---------------- | ------- | ------- | -------------------------------------------------------- |
| `channels`       | CSV     | —       | Filter by channel (e.g. `ctv,olv,display`)               |
| `property_types` | CSV     | —       | Filter by property type (e.g. `ctv_app,website`)         |
| `markets`        | CSV     | —       | Filter by market/country code (e.g. `US,GB`)             |
| `categories`     | CSV     | —       | Filter by IAB content category (e.g. `IAB-7,IAB-7-1`)    |
| `tags`           | CSV     | —       | Filter by tag (e.g. `premium,brand_safe`)                |
| `delivery_types` | CSV     | —       | Filter by delivery type (e.g. `guaranteed,programmatic`) |
| `has_tmp`        | boolean | —       | Require TMP support (`true` or `false`)                  |
| `min_properties` | number  | —       | Minimum number of properties in inventory                |
| `cursor`         | string  | —       | Pagination cursor from a previous response               |
| `limit`          | number  | 50      | Max results per page (1–200)                             |

Each CSV parameter accepts up to 100 values.

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://agenticadvertising.org/api/registry/agents/search?channels=ctv,olv&markets=US&has_tmp=true" \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    "https://agenticadvertising.org/api/registry/agents/search?channels=ctv,olv&markets=US&has_tmp=true",
    { headers: { Authorization: "Bearer YOUR_API_KEY" } }
  );
  const agents = await res.json();
  ```

  ```python Python theme={null}
  import requests

  agents = requests.get(
      "https://agenticadvertising.org/api/registry/agents/search",
      headers={"Authorization": "Bearer YOUR_API_KEY"},
      params={"channels": "ctv,olv", "markets": "US", "has_tmp": "true"},
  ).json()
  ```
</CodeGroup>

```json Response theme={null}
{
  "results": [
    {
      "agent_url": "https://ads.streamhaus.example.com",
      "channels": ["ctv", "olv"],
      "property_types": ["ctv_app", "website"],
      "markets": ["US", "GB", "CA"],
      "categories": ["IAB-7", "IAB-7-1"],
      "tags": ["premium"],
      "delivery_types": ["guaranteed"],
      "format_kinds": ["video_hosted", "video_vast"],
      "property_count": 42,
      "publisher_count": 3,
      "has_tmp": true,
      "category_taxonomy": null,
      "relevance_score": 0.92,
      "matched_filters": ["channels", "markets"],
      "updated_at": "2026-03-31T10:00:00.000Z"
    }
  ],
  "cursor": "MC45Mjpodh...",
  "has_more": false
}
```

The `matched_filters` array shows which filter dimensions matched, useful for understanding why a result was returned. The `relevance_score` combines filter match breadth, `ln(property_count + 1)` weighted at 0.1, and a 0.05 boost for TMP support.

#### Crawl request

`POST /api/registry/crawl-request`

Request an immediate re-crawl of a publisher domain. Use this after updating an `adagents.json` file so the registry picks up changes without waiting for the next scheduled crawl. The crawl runs asynchronously — the endpoint returns `202 Accepted` immediately.

Rate-limited to one request per domain every 5 minutes and 30 requests per user per hour.

**Request body:**

```json theme={null}
{
  "domain": "examplepub.com"
}
```

`domain` is required. Domains are normalized (lowercased, trimmed). The endpoint validates the domain format and performs a DNS lookup to reject private/reserved IP addresses.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://agenticadvertising.org/api/registry/crawl-request" \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"domain":"examplepub.com"}'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    "https://agenticadvertising.org/api/registry/crawl-request",
    {
      method: "POST",
      headers: {
        "Authorization": "Bearer YOUR_API_KEY",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ domain: "examplepub.com" }),
    }
  );
  const result = await res.json(); // 202
  ```

  ```python Python theme={null}
  import requests

  result = requests.post(
      "https://agenticadvertising.org/api/registry/crawl-request",
      headers={"Authorization": "Bearer YOUR_API_KEY"},
      json={"domain": "examplepub.com"},
  ).json()
  ```
</CodeGroup>

```json 202 Accepted theme={null}
{
  "message": "Crawl request accepted",
  "domain": "examplepub.com"
}
```

```json 429 Too Many Requests theme={null}
{
  "error": "Rate limit exceeded for this domain",
  "retry_after": 245
}
```

`retry_after` is the number of seconds to wait before retrying.

#### Submit brand (legacy)

`POST /api/brands/discovered/community`

Submit a brand for review. This endpoint predates `/api/brands/save` — prefer the save endpoint for new integrations.

### Error responses

| Status | Description                                                                               |
| ------ | ----------------------------------------------------------------------------------------- |
| 400    | Missing required fields or invalid domain                                                 |
| 401    | Missing or invalid API key                                                                |
| 409    | Cannot edit an authoritative brand/property (managed via `brand.json` or `adagents.json`) |
| 410    | Cursor expired (change feed — older than 90-day retention window)                         |
| 429    | Rate limit exceeded                                                                       |

## Protocol vs REST API

The AdCP protocol defines MCP and A2A tasks for agent-to-agent communication (e.g. `get_products`, `create_media_buy`). The registry REST API is separate — it provides HTTP endpoints for looking up entities in the AgenticAdvertising.org registry.

**Use the REST API** for discovery and authorization:

* Resolve brand or property domains before making protocol calls
* Discover which agents exist and what they're authorized for
* Validate authorization in real time during ad serving
* Build integrations that browse or search the registry

**Use MCP/A2A tasks** for transactional operations:

* Fetching products from a sales agent (`get_products`)
* Creating media buys (`create_media_buy`)
* Building creatives (`build_creative`)
* Getting signals (`get_signals`)

A typical integration uses both: resolve a publisher domain via the registry API, then call the authorized agent's MCP endpoint to transact.
