AudiencePlan Wire-Format Specification¶
Status: Active
Proposal refs: §5.1, §5.6, §5.7, §6 rows 14b/15, §7
This document is the authoritative wire-format definition for AudiencePlan over HTTP.
Code in both repos cites it by section number; do not renumber sections.
§1 Purpose and Scope¶
AudiencePlan is the buyer-emitted audience targeting payload threaded through deal
booking. It is produced by the Audience Planner agent and attached to
DealBookingRequest (buyer side) / accepted by POST /api/v1/deals (seller side).
The plan carries one primary audience plus optional constraint, extension, and exclusion
audiences composed from three ref types (standard, contextual, agentic).
This document specifies:
- The JSON schema for
AudiencePlanandAudienceRef(§2). - The HTTP content-type negotiation contract (§3).
- The OpenRTB v2.6 translation for impression-time targeting (§4).
- The booking-time lifecycle and forensic logging (§5).
- A stable field-reference table (§6) cited by code comments.
- The seller rejection / buyer fallback protocol (§7).
- Versioning strategy (§8).
- Migration policy for the OpenRTB agentic extension (§9).
§2 JSON Schema¶
§2.1 AudiencePlan¶
Top-level object carried in DealBookingRequest.audience_plan.
{
"schema_version": "1",
"audience_plan_id": "sha256:<hex>",
"primary": { ... },
"constraints": [ ... ],
"extensions": [ ... ],
"exclusions": [ ... ],
"rationale": "Human-readable planner explanation"
}
| Field | Type | Required | Description |
|---|---|---|---|
schema_version |
string |
no | Schema version; currently "1". Bumped on breaking changes. Default "1". |
audience_plan_id |
string |
no | sha256:<hex> content hash (see §2.3). Auto-computed when blank. |
primary |
AudienceRef |
yes | The primary audience for the campaign. |
constraints |
AudienceRef[] |
no | Refs that intersect with primary (precision). Default []. |
extensions |
AudienceRef[] |
no | Refs that union with primary (reach). Default []. |
exclusions |
AudienceRef[] |
no | Refs subtracted from the assembled audience. Default []. |
rationale |
string |
no | Human-readable explanation; excluded from the hash. Default "". |
See src/ad_buyer/models/audience_plan.py (AudiencePlan).
§2.2 AudienceRef¶
A single audience reference. The type field is the discriminator.
{
"type": "standard",
"identifier": "3-7",
"taxonomy": "iab-audience",
"version": "1.1",
"source": "explicit",
"confidence": null,
"compliance_context": null
}
| Field | Type | Required | Description |
|---|---|---|---|
type |
"standard" \| "contextual" \| "agentic" |
yes | Ref type; discriminates identifier semantics. |
identifier |
string |
yes | IAB Audience Taxonomy ID (standard), IAB Content Taxonomy ID (contextual), or embedding URI (agentic, e.g. emb://…). |
taxonomy |
string |
yes | "iab-audience" | "iab-content" | "agentic-audiences" |
version |
string |
yes | Taxonomy version, e.g. "1.1", "3.1", "draft-2026-01". |
source |
"explicit" \| "resolved" \| "inferred" |
yes | Provenance of the ref. |
confidence |
float \| null |
no | Match confidence in [0, 1]. MUST be null when source="explicit". |
compliance_context |
ComplianceContext \| null |
conditional | Required when type="agentic"; optional otherwise. |
See src/ad_buyer/models/audience_plan.py (AudienceRef) and the mirrored
src/ad_seller/models/audience_ref.py.
§2.2.1 ComplianceContext¶
Consent regime accompanying an AudienceRef. Required for type=agentic.
| Field | Type | Required | Description |
|---|---|---|---|
jurisdiction |
string |
yes | Jurisdiction code: "US", "EU", "GLOBAL". |
consent_framework |
string |
yes | "IAB-TCFv2", "GPP", "advertiser-1p", "none". |
consent_string_ref |
string \| null |
no | Opaque pointer to the consent string (not the raw string). |
attestation |
string \| null |
no | Hash or signature for required attestation. |
embedding_provenance |
string \| null |
no | "local_buyer", "advertiser_supplied", "hosted_external", or "mock". |
Note: agentic refs with jurisdiction="GLOBAL" are rejected at brief ingestion
(see validate_no_global_agentic in src/ad_buyer/models/audience_plan.py). A single
ComplianceContext cannot honestly span multiple consent regimes. Until per-jurisdiction
fan-out lands (proposal §7), separate refs per jurisdiction are required.
§2.3 compute_id() Determinism¶
AudiencePlan.audience_plan_id is the SHA-256 hash of the plan's canonical content:
- Extract roles
{primary, constraints, extensions, exclusions}viamodel_dump(mode="json"). - Recursively sort all dict keys (canonical form); list order is preserved — planner role-ordering is semantically significant.
- Serialize with
json.dumps(sort_keys=True, separators=(",", ":"), ensure_ascii=False)and encode UTF-8. - Compute
hashlib.sha256(payload).hexdigest()and prefix with"sha256:".
Fields excluded from the hash: audience_plan_id itself, schema_version,
rationale.
Result is stable across Pydantic field-order changes. It is not stable across reorderings of list items within a role.
See src/ad_buyer/models/audience_plan.py:compute_id().
§3 Content-Type Negotiation¶
Two media types are accepted on audience_plan-bearing requests (see
src/ad_buyer/clients/deals_client.py:63-65 and
src/ad_seller/interfaces/api/main.py:39-41):
| Name | Media Type |
|---|---|
| Legacy UCP carrier | application/vnd.ucp.embedding+json; v=1 |
| IAB Agentic Audiences | application/vnd.iab.agentic-audiences+json; v=1 |
Buyer emission (booking-time):
Content-Type: application/vnd.ucp.embedding+json; v=1— the legacy UCP carrier remains the emit name during the transition window for backward compatibility with sellers that predate the rename (proposal §5.6 lock #1).Accept: application/vnd.ucp.embedding+json; v=1, application/vnd.iab.agentic-audiences+json; v=1— advertises that the buyer can read either name on the seller's response.
Bookings without audience_plan keep the default Content-Type: application/json
headers; this path is unchanged.
Seller acceptance:
Both names parse via the same JSON body. FastAPI's JSON body parser reads the body
regardless of Content-Type, so both are accepted without custom middleware. Servers
MAY echo the request's Content-Type on the response. See
src/ad_seller/interfaces/api/main.py:38-41.
§4 OpenRTB v2.6 Translation¶
After a deal is booked, the buyer translates the AudiencePlan into OpenRTB v2.6
fragments for impression-time bid requests. The builder
(src/ad_buyer/clients/openrtb_builder.py) returns a dict with "user" and/or
"site" keys that the campaign runtime merges into a full BidRequest.
| Ref type | OpenRTB carrier | Notes |
|---|---|---|
standard |
user.data[].segment[] |
All standard refs grouped under a single user.data entry with name="IAB_Taxonomy" and ext.taxonomy_version set to the first ref's version. |
contextual |
site.cat[] + site.cattax=7 |
cattax=7 is the OpenRTB 2.6 enum for IAB Content Taxonomy 3.1. Contextual exclusions are dropped (OpenRTB site.cat has no exclusion semantics) with a structured logger.warning. |
agentic |
user.ext.iab_agentic_audiences.refs[] |
Feature-flagged; emitted only when enable_agentic_openrtb_ext=True (default off). When disabled, agentic refs are dropped with a structured warning. See §9. |
§4.1 Standard Ref Mapping¶
{
"user": {
"data": [
{
"name": "IAB_Taxonomy",
"ext": {"taxonomy_version": "1.1"},
"segment": [
{"id": "3-7"},
{"id": "3-8", "value": "0.85"},
{"id": "3-9", "ext": {"exclude": true}}
]
}
]
}
}
Exclusion segments carry ext.exclude=true (ad-hoc; sellers MAY honor).
Confidence values from resolved/inferred refs surface as "value" (string per
OpenRTB segment schema).
§4.2 Contextual Ref Mapping¶
§4.3 Agentic Ref Mapping (feature-flagged)¶
When enabled (enable_agentic_openrtb_ext=True in buyer settings):
{
"user": {
"ext": {
"iab_agentic_audiences": {
"refs": [
{
"identifier": "emb://buyer.example.com/audiences/sports-fans-v3",
"version": "draft-2026-01",
"source": "explicit",
"compliance_context": {
"jurisdiction": "US",
"consent_framework": "GPP",
"consent_string_ref": null,
"attestation": null,
"embedding_provenance": "local_buyer"
}
}
]
}
}
}
}
The namespaced key iab_agentic_audiences is temporary pending IAB ratification.
See §9 for the 90-day dual-emit migration policy.
See src/ad_buyer/clients/openrtb_builder.py.
§5 Lifecycle¶
Buyer Seller
| |
|--- POST /api/v1/deals ------------------>|
| Content-Type: vnd.ucp.embedding+json |
| body: { ..., "audience_plan": {...} } |
| |
| validate plan |
| against capabilities|
| |
| (if unsupported) |
|<-- 400 audience_plan_unsupported --------|
| (buyer degrades plan and retries) |
| |
| (if supported) |
| freeze snapshot |
| compute match |
| log plan_id hash |
|<-- 200 DealResponse ---------------------|
| audience_plan_snapshot |
| audience_match_summary |
| |
| log plan_id hash |
Both sides log the audience_plan_id hash at booking time (INFO level):
- Buyer:
ad_buyer.audience.bookinglogger, message:deal_booking audience_plan_id=<hash> quote_id=<id>(seesrc/ad_buyer/clients/deals_client.py:234-238) - Seller:
ad_seller.audience.bookinglogger, message:deal_booking deal_id=<id> audience_plan_id=<hash> quote_id=<id>(seesrc/ad_seller/interfaces/api/main.py:2513-2518)
Matching log entries across both systems are the forensic anchor for post-booking dispute resolution about what was frozen at booking time.
§6 Field Reference Table¶
Rows are cited by number in code comments. Do not reorder or renumber.
| Row | Surface | Field / Behavior | Source |
|---|---|---|---|
| 1 | AudiencePlan |
schema_version |
models/audience_plan.py |
| 2 | AudiencePlan |
audience_plan_id (sha256-prefixed hash) |
models/audience_plan.py:compute_id() |
| 3 | AudiencePlan |
primary (required AudienceRef) |
models/audience_plan.py |
| 4 | AudiencePlan |
constraints[] |
models/audience_plan.py |
| 5 | AudiencePlan |
extensions[] |
models/audience_plan.py |
| 6 | AudiencePlan |
exclusions[] |
models/audience_plan.py |
| 7 | AudiencePlan |
rationale (excluded from hash) |
models/audience_plan.py |
| 8 | AudienceRef |
type discriminator (standard/contextual/agentic) |
models/audience_plan.py |
| 9 | AudienceRef |
identifier (taxonomy ID or embedding URI) |
models/audience_plan.py |
| 10 | AudienceRef |
compliance_context (required on type=agentic) |
models/audience_plan.py |
| 11 | Seller validation | Per-role capability gating + cardinality cap | services/audience_plan_validator.py |
| 12 | Content-Type | application/vnd.ucp.embedding+json; v=1 (emit name) |
clients/deals_client.py:63 |
| 13 | Content-Type | application/vnd.iab.agentic-audiences+json; v=1 (alias) |
clients/deals_client.py:64 |
| 14b | Forensic logging | Buyer + seller both log audience_plan_id hash at booking (INFO) |
clients/deals_client.py:234, interfaces/api/main.py:2513 |
| 15 | OpenRTB agentic ext | enable_agentic_openrtb_ext feature flag (default off) |
clients/openrtb_builder.py:99 |
§6.5 audience_match_summary¶
The seller's deal booking response includes audience_match_summary when an
audience_plan was present in the request. Shape:
{
"audience_match_summary": {
"primary": {"match": "STRONG", "score": 0.91},
"constraints": [{"match": "MODERATE", "score": 0.72}],
"extensions": [],
"exclusions": []
}
}
Match bucket labels: STRONG (≥ 0.85), MODERATE (≥ 0.65), WEAK (≥ 0.40),
NONE (< 0.40). Empty arrays MAY be omitted by the server but receivers MUST treat
absence as empty (the buyer's AudienceMatchSummary model defaults all roles to []).
See src/ad_seller/interfaces/api/main.py:_build_audience_match_summary() and
src/ad_buyer/models/deals.py:AudienceMatchSummary.
§7 Rejection and Fallback¶
If the seller receives an audience_plan with any part it cannot honor, it returns
HTTP 400 with a structured audience_plan_unsupported body (proposal §5.7 layer 3):
{
"detail": {
"error": "audience_plan_unsupported",
"unsupported": [
{"path": "extensions[0]", "reason": "extensions not supported by this seller"},
{"path": "primary.taxonomy", "reason": "version '3.2' not supported (seller supports ['1.1', '3.1'])"}
]
}
}
The unsupported list contains {"path": str, "reason": str} entries for every
unsupported element. The path syntax mirrors the plan's JSON structure
(primary.taxonomy, constraints[0].taxonomy, extensions, exclusions).
Buyer behavior: DealsClient._build_error_from_response() parses the FastAPI-
wrapped detail shape and surfaces the unsupported list on
DealsClientError.unsupported. The buyer's orchestrator calls
degrade_plan_for_seller() against the precise drops the seller listed, then retries.
See src/ad_buyer/clients/deals_client.py:78 (DealsClientError.unsupported).
Seller validation logic: src/ad_seller/services/audience_plan_validator.py
validates taxonomy versions, per-role gating, and cardinality caps against the seller's
CapabilityAudienceBlock.
§8 Versioning¶
The v=1 parameter in both media types signals schema version 1. The schema_version
field in the JSON body echoes this ("1" currently).
Forward-compatibility strategy:
- The seller MUST ignore unknown fields in
AudiencePlanandAudienceRef(Pydantic's defaultextra="ignore"on the seller's dict-based acceptance). - Unknown
AudienceRef.typevalues are reported as unsupported via the structured rejection in §7 (forward-compat: the seller doesn't know how to honor them). - When a breaking schema change is needed, increment
v=in the media type tov=2and bumpschema_version. Both sides MUST continue acceptingv=1during the transition window (length TBD per IAB Tech Lab process).
§9 Migration Policy: OpenRTB Agentic Extension¶
The user.ext.iab_agentic_audiences key (§4.3) is a temporary namespaced extension
pending IAB ratification of a standard OpenRTB agentic audience slot.
90-day dual-emit policy: Once IAB ratifies an extension key, the buyer will:
- Emit the ratified key in addition to
iab_agentic_audiencesfor 90 days (dual-emit window). - At day 90, drop
iab_agentic_audiencesand emit only the ratified key.
Sellers SHOULD accept both keys during the dual-emit window and prefer the ratified key.
The dual-emit is gated by the same enable_agentic_openrtb_ext flag (row 15 above)
so deployments that do not want to ship either key during the transition can hold the
flag off. See src/ad_buyer/clients/openrtb_builder.py:6-28 for the current policy
comment.
Related¶
- Deals API Client —
DealsClientusage and quote-then-book flow - Bookings API — Campaign booking workflow (brief-based)
src/ad_buyer/models/audience_plan.py— Buyer-side model andcompute_id()src/ad_buyer/clients/deals_client.py— Content-type emission and forensic loggingsrc/ad_buyer/clients/openrtb_builder.py— OpenRTB translationsrc/ad_seller/interfaces/api/main.py— Seller acceptance, validation, and loggingsrc/ad_seller/services/audience_plan_validator.py— Structured rejection logicsrc/ad_seller/models/audience_ref.py— Seller-side mirror ofAudienceRefdocs/proposals/AUDIENCE_PLANNER_3TYPE_EXTENSION_2026-04-25.md— Upstream proposal