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

# Specification Guidelines

> AdCP specification guidelines: type naming rules, discriminated union patterns, field naming conventions, and style standards for writing protocol spec pages.

# AdCP Specification Guidelines

This document outlines design principles and rules for maintaining the AdCP specification. These guidelines help ensure consistency, clarity, and ease of implementation across different programming languages.

## Type Naming Principles

### No Reused Type Names

**RULE**: Never use the same enum name or field name to represent different concepts, even in different contexts.

**Why**: Type generators (TypeScript, Python, Go, etc.) create collisions when the same name appears with different values or semantics. This forces downstream users to use awkward workarounds like aliasing or deep imports.

**Example of the problem**:

```json theme={null}
// ❌ BAD: Multiple "Type" enums with different meanings
// asset-type.json
{ "type": "string", "enum": ["image", "video", "html"] }

// format.json
{ "type": "string", "enum": ["audio", "video", "display"] }

// Result: Python generates Type, Type1, Type2 or uses alphabetical first-wins
```

**Solution**: Use semantic, domain-specific names:

```json theme={null}
// ✅ GOOD: Distinct enum names for different concepts
// asset-content-type.json
{ "type": "string", "enum": ["image", "video", "html"] }

// pricing-model.json
{ "type": "string", "enum": ["cpm", "cpc", "fixed"] }

// Result: Python generates AssetContentType and PricingModel
```

### Semantic Field Names

Field names should describe **what** they represent, not generic categories.

**Examples**:

* ✅ `asset_content_type` - Clear: describes what content the asset contains
* ❌ `type` - Ambiguous: type of what?
* ❌ `asset_type` - Better, but could conflict with other type fields

### Enum Consolidation

When the same concept appears in multiple places with different subsets:

1. **Create a single canonical enum** with all possible values
2. **Reference that enum** in all schemas using `$ref`
3. **Document subset expectations** in field descriptions when needed

**Example**:

```json theme={null}
// enums/asset-content-type.json - Single source of truth
{
  "$id": "/schemas/v3/enums/asset-content-type.json",
  "type": "string",
  "enum": ["image", "video", "audio", "text", "html", "javascript", ...]
}

// brand.json - References full enum
{
  "asset_type": {
    "$ref": "/schemas/v3/enums/asset-content-type.json",
    "description": "Type of asset. Note: Brand manifests typically contain basic media assets (image, video, audio, text)."
  }
}

// list-creative-formats-request.json - References full enum
{
  "asset_types": {
    "type": "array",
    "items": {
      "$ref": "/schemas/v3/enums/asset-content-type.json"
    }
  }
}
```

**Benefits**:

* Type generators produce single, consistent types
* API allows filtering/specifying any valid value
* Adding new values is non-breaking
* Documentation clarifies typical usage without restricting capability

## Specialist Module Naming

### Core Principle

Specialist module names must reflect the **technical capability** being taught — what practitioners verify, resolve, or operate — not the business or marketing category.

A module titled "Brand" is ambiguous: does it teach brand-safety policy, brand-identity schema validation, or brand-campaign strategy? A module titled "Brand Identity & Verification" tells a developer exactly what they will learn to do.

### Naming Consistency

The module name must be consistent across all four surfaces where it appears:

1. **Page title** — the `title:` frontmatter in the module's `.mdx` file
2. **Badge** — the `adcp_specialist_*` credential suffix (e.g., `adcp_specialist_signals`)
3. **Sidebar navigation** — the `sidebarTitle:` frontmatter
4. **Specialist overview table** — the row in the certification overview page

If any of these diverge, implementers looking at one surface form a different mental model than those reading another. Keep all four in sync.

### Good vs. Bad Names

| Avoid | Prefer                        | Why                                                                                            |
| ----- | ----------------------------- | ---------------------------------------------------------------------------------------------- |
| Brand | Brand Identity & Verification | "Brand" reads as marketing; the module teaches schema validation and identity resolution       |
| Ads   | Creative Asset Management     | "Ads" is too broad; the module covers creative formats, asset pipelines, and approval flows    |
| Data  | Signals & Audience Activation | "Data" is generic; the module teaches signal discovery, privacy controls, and activation loops |

### Naming Checklist

Before proposing a new specialist module:

* [ ] Does the name describe the technical workflow, not the business domain?
* [ ] Would a developer unfamiliar with AdCP understand what the module teaches from the name alone?
* [ ] Is the name consistent across page title, badge, sidebar, and overview table?
* [ ] Does the badge suffix (`adcp_specialist_*`) read naturally in a credential context?

## Enum Design

### Enum File Structure

All enums should live in `/schemas/v3/enums/` with descriptive names:

```
/schemas/v3/enums/
  asset-content-type.json      # What IS this asset?
  pricing-model.json           # How is this PRICED?
  media-buy-status.json        # What STATE is the buy in?
```

### Enum Naming Convention

* Use **noun phrases** that describe what's being categorized
* Use **kebab-case** for filenames
* Generated type names use **PascalCase** (AssetContentType, PricingModel)
* Avoid generic terms like "type", "kind", "status" without qualifiers

### When to Create a New Enum

Create a dedicated enum file when:

* Values are reused across multiple schemas
* Values represent a closed set of options
* The concept is fundamental to the protocol
* Type safety would benefit implementers

### Enum membership — when to add a value

Adding a value to an *existing* enum is a curation decision, not a default. An enum is a curated roster of real, shared semantics — not a registry of every vendor or integration. A value earns membership when **all** hold:

* **Published** — it names an externally-documented concept with a stable definition, not a per-buyer or per-integration shape.
* **Natively supported** — at least one real implementer handles it directly, without bespoke per-value mapping (for a `feed_format`, the seller parses it natively without `feed_field_mappings`).
* **Shared demand** — it is relevant across more than one producer **and** more than one consumer (a shared dialect, not branding for a single bilateral integration).

A material **dialect** of an existing value earns its own value only when its differences would make the parent value's consumer mis-handle it — a renamed primary key, composite-encoded fields, or a field the parent treats as optional but the dialect requires. Cosmetic or additive-optional differences do not; use the parent value. When a concept fails these tests, model it through the schema's existing extension path (`custom` + a mapping, or `ext`) rather than minting an enum value.

This is distinct from [Platform Agnosticism](#platform-agnosticism): a `feed_format` value legitimately names a vendor's *published spec* (the value **is** the spec), whereas platform-agnosticism forbids a vendor-specific *version of a general concept*.

**Worked example — `feed_format` ([#3456](https://github.com/adcontextprotocol/adcp/issues/3456)).** `tiktok_shop`, `pinterest_catalog`, and `openai_product_feed` qualify: published, Google-Merchant-Center-derived feed dialects that real sellers parse natively, each with deltas a strict GMC parser would mis-handle. A feed without a published, natively-parsed spec uses `custom` + `feed_field_mappings`.

## Field Design

### Discriminated Unions

When objects can have multiple shapes, always use explicit discriminator fields:

```json theme={null}
{
  "oneOf": [
    {
      "type": "object",
      "properties": {
        "delivery_type": { "type": "string", "const": "url" },
        "url": { "type": "string" }
      },
      "required": ["delivery_type", "url"]
    },
    {
      "type": "object",
      "properties": {
        "delivery_type": { "type": "string", "const": "inline" },
        "content": { "type": "string" }
      },
      "required": ["delivery_type", "content"]
    }
  ]
}
```

This enables proper type narrowing in TypeScript and pattern matching in other languages.

### Avoiding Over-Specific Subsets

Don't artificially restrict enum values in request schemas unless there's a technical reason:

* ❌ Limit `asset_types` filter to 7 values "because most people only use these"
* ✅ Allow all asset content types - let users filter by anything

If certain values are uncommon, document that in the description but don't prevent their use.

## Schema References

### When to Use \$ref

Use `$ref` for:

* Enum values (always)
* Core data models used in multiple places
* Complex nested objects used repeatedly

Don't use `$ref` for:

* Simple inline objects used only once
* Request-specific parameters
* Highly contextual structures

### Reference Paths

All `$ref` paths should be absolute from schema root:

```json theme={null}
// ✅ GOOD: Absolute path
"$ref": "/schemas/v3/enums/asset-content-type.json"

// ❌ BAD: Relative path
"$ref": "../../enums/asset-content-type.json"
```

## Platform Agnosticism

**RULE**: Normative schema **field names** MUST NOT represent a specific vendor's version of a general concept. Platform-specific fields belong under `ext.{vendor}`.

**Why**: AdCP is a protocol, not a platform. A field named `google_campaign_id` or `ttd_line_id` at the top level of a schema bakes one vendor's data model into the spec and creates lock-in. The protocol is credible as an open standard only to the extent that its normative field surface is vendor-neutral.

**How**: Vendor-specific fields belong in the `ext.{vendor}` namespace (schema: `/schemas/core/ext.json`, source: `static/schemas/source/core/ext.json`). `ext` is `additionalProperties: true` — the namespacing is a convention enforced by review, not by JSON Schema.

```json theme={null}
// ❌ BAD: vendor name in a normative field (a general concept dressed up as a vendor)
{
  "google_campaign_id": "abc123"
}

// ✅ GOOD: vendor-specific under ext
{
  "ext": {
    "gam": { "campaign_id": "abc123" }
  }
}
```

### External system identifiers

Names that reference **canonical external identifier spaces** are legitimate in both field names and enum values. The distinction is not "does it contain a vendor token" but "does it represent *that vendor's version of something the protocol already has a general concept for*":

* `google_campaign_id` (bad) — a vendor-specific ID for a concept the protocol already models (`media_buy_id`). Move to `ext.gam`.
* `apple_podcast_id` (legitimate) — a canonical identifier for a specific Apple Podcasts item. There is no general concept to map to; the Apple Podcasts namespace is *the* namespace.
* `nielsen_dma` (legitimate) — the industry-standard geographic division, not "Nielsen's version of geography."

Existing examples of legitimate patterns:

* Distribution-platform identifier types: `amazon_music_id`, `roku_channel_id` in `distribution-identifier-type.json` (enum values)
* Feed formats: `google_merchant_center`, `facebook_catalog` in `brand.json` (enum values) — widely-adopted open interchange formats implemented by many third parties
* Measurement/data identifiers: `nielsen_dma` in `get-adcp-capabilities-response` (field name)
* Platform IDs: `apple_podcast_id`, `apple_id` (field names)

The rule to apply: if the name asks "which vendor-equivalent version of something AdCP models?" (bad — use `ext`), reject; if the name asks "which externally-defined system/format/identifier space?" (legitimate), allow. When allowing a field name, add it to `tests/check-platform-agnostic.cjs` `FIELD_ALLOWLIST` with a one-line justification. When allowing an enum value, add it to `ENUM_VALUE_ALLOWLIST` with a path-qualified entry and a one-line justification.

### Reviewer checklist

* Reject a new top-level or request/response field whose name is `{vendor}_{general_concept}` (e.g., `google_campaign_id`, `ttd_line_id`).
* Accept an enum value naming an externally-defined system, format, or identifier space.
* Vendor names in **example blocks** (email addresses, sample IDs) are fine.
* When uncertain, ask: "Is this field or value representing *one vendor's version of something the protocol already has a general concept for*?" If yes, it belongs under `ext.{vendor}`.

## Reserved SDK-Internal Keys

**RULE**: The top-level key `ctx_metadata` is reserved on AdCP resource objects as an adapter-internal round-trip cache for state that an SDK or platform adapter needs to carry across calls but that buyers MUST NOT see or rely on. Adapters MUST strip `ctx_metadata` from any payload before wire egress. When the key was present and non-empty at strip time, adapters MUST emit a warning-level log entry so operators can detect accidental key collisions with custom adapter code. (An empty or absent `ctx_metadata` is silent — only a non-empty value triggers the warning.)

**Why**: Platform adapters (e.g. Google Ad Manager, Kevel, custom seller infrastructure) often need to associate adapter-internal identifiers — GAM ad-unit IDs, key-value pairs, placement IDs — with AdCP resources that the buyer-facing SDK returns. The reference Prebid `salesagent` Python implementation uses an `implementation_config` JSON column on its Product model for exactly this purpose. Without a reserved name, every SDK invents its own (`implementation_config`, `_internal`, `sdk_state`, etc.); a fourth SDK then collides with one of them, or two SDKs converging on the same name produce ambiguous semantics. One reserved name removes the coordination problem.

**Scope**: The reservation applies to AdCP resource objects whose schemas declare `additionalProperties: true` — including `Product`, `MediaBuy`, `Package`, `Creative`, `AudienceSegment`, `Signal`, and `RightsGrant`. The reservation travels with the resource wherever it appears: top-level in a response envelope, nested inside another resource (e.g. `Package` inside `MediaBuy`), or inside an array of resources (e.g. each element of `products: Product[]`). Adapters MUST strip the key from every occurrence before egress, not just the outermost one.

`PropertyList` and `CollectionList` declare `additionalProperties: false` and are out of scope until a follow-up PR widens those schemas; until then, adapters needing round-trip state for those resources should track it out-of-band.

**Distinction from neighboring conventions**:

* `ext.{vendor}` — vendor-namespaced, **buyer-visible**, travels on the wire. Use for vendor-specific data the buyer should see (e.g. `ext.gam.line_item_id`).
* `context` / `context_id` — caller-echoed correlation data, also wire-visible. Despite the prefix-match, `ctx_metadata` is not a sub-namespace of these — they are unrelated concepts and travel on different layers.
* `ctx_metadata` — **adapter-internal only**, MUST be stripped before egress, never reaches the buyer.

**Adapter conformance**:

```
1. Read ctx_metadata from inbound resource (publisher → SDK direction).
2. Carry it in adapter-local state.
3. Before serializing the resource for wire egress (SDK → buyer direction):
   a. Remove the ctx_metadata key.
   b. If the key was present and non-empty, emit a warning-level log:
      "stripping reserved ctx_metadata before egress on <resource_type>"
4. Buyer-facing surfaces MUST NOT expose ctx_metadata in any documentation,
   typed shape, or example.
```

**Reviewer checklist**:

* Reject any spec, schema, or example that promotes `ctx_metadata` as a buyer-readable field.
* Reject any SDK contribution that surfaces `ctx_metadata` in a buyer-facing typed return.
* Accept SDK code that reads/writes `ctx_metadata` as adapter-internal state, provided the egress-strip + warning-log path is in place.

## Breaking Changes

### What Constitutes a Breaking Change

**Major version bump required**:

* Removing enum values
* Renaming fields
* Changing field types
* Making optional fields required
* Removing fields entirely

**Minor version bump allowed**:

* Adding new enum values (append-only)
* Adding new optional fields
* Clarifying descriptions
* Adding new tasks/endpoints

### Migration Strategy

When making breaking changes:

1. **Create v2 directory**: `/schemas/v3/`
2. **Maintain v1**: Keep old schemas functional
3. **Document migration**: Provide before/after examples
4. **Deprecation period**: Support both versions for defined period

## JSON Schema Conventions

### Nullable Scalars

For AdCP 3.x draft-07 schemas, encode nullable scalar fields as a JSON Schema
type union:

```json theme={null}
{ "type": ["string", "null"] }
```

Use the same pattern for nullable numbers, integers, booleans, and mixed scalar
value buckets. Do not introduce OpenAPI-style `nullable: true` in source
schemas; it is not part of JSON Schema Draft 07 and creates inconsistent SDK
projection rules.

Nullable enums must include `null` in both the `type` union and the `enum` value
set:

```json theme={null}
{
  "type": ["string", "null"],
  "enum": ["active", "paused", null]
}
```

Nullability and presence are separate in JSON Schema Draft 07:

* `type: ["string", "null"]` means the field may be `null` when it is present.
* The enclosing object's `required` array controls whether the field must be present.
* Optional nullable fields therefore have three states: omitted, present with `null`,
  and present with a scalar value.
* Required nullable fields have two states: present with `null` or present with a
  scalar value.

When omission and explicit `null` carry different semantics, state that distinction
in the field description so SDK generators do not collapse the cases.

## Testing Schemas

All schema changes must:

1. ✅ Validate with JSON Schema Draft 07
2. ✅ Pass example data through validation
3. ✅ Generate types successfully (Python, TypeScript)
4. ✅ Update documentation to match
5. ✅ Include an `adcontextprotocol` changeset describing the schema change

## Review Checklist

Before merging schema changes, verify:

* [ ] No duplicate enum names across different files
* [ ] No ambiguous field names (like bare "type")
* [ ] All enums referenced via `$ref`, not inline
* [ ] Breaking changes use proper versioning
* [ ] Documentation updated to match schemas
* [ ] Examples validate against new schemas
* [ ] Type generation tested
* [ ] `adcontextprotocol` changeset created with proper version bump

## Philosophy

**"The schema is the spec"**

Documentation should reflect what's in schemas, but schemas are the source of truth. When documentation and schemas diverge, schemas win. This means:

* Write clear, detailed descriptions in schemas
* Use semantic names that are self-documenting
* Design for type generation, not just validation
* Think about developer ergonomics across languages

**"Make the right thing easy"**

Good schema design guides implementers toward correct usage:

* Use discriminators so type checkers catch mistakes
* Use semantic names so code reads clearly
* Consolidate enums so generators produce clean types
* Restrict where necessary, but don't over-restrict

## Questions?

When in doubt about schema design decisions:

1. Check existing patterns in `/schemas/v3/`
2. Consider impact on type generation
3. Ask: "Will this name collision cause issues?"
4. Prefer specificity over brevity
5. Document rationale in this file for future reference
