Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1aafe64
feat(reporting): add managed reporting status schemas
bokelley Aug 27, 2026
c1733e9
fix(reporting): harden managed delivery contract
bokelley Aug 28, 2026
a57960f
feat(reporting): add consumer reconciliation receipts
bokelley Aug 28, 2026
1095b72
fix(reporting): complete receipt tool integration
bokelley Aug 28, 2026
7fda761
fix(reporting): type receipt response status
bokelley Aug 28, 2026
7001b1a
fix(reporting): require manifest resource identity
bokelley Aug 28, 2026
abef289
test(reporting): refresh MCP schema analysis counts
bokelley Aug 28, 2026
afca491
fix(reporting): make delivery independently reconcilable
bokelley Aug 28, 2026
592c854
feat(reporting): declare snapshot and mixed-source coverage
bokelley Aug 29, 2026
a416550
fix(reporting): represent empty configuration scopes
bokelley Aug 29, 2026
2cb411b
test(reporting): refresh schema analysis after rebase
bokelley Aug 29, 2026
1fd3099
docs(accounts): fix released schema links
bokelley Aug 29, 2026
524fde0
Merge origin/main into managed reporting status
bokelley Aug 29, 2026
cbddfd9
fix(reporting): enforce reconciliation integrity schemas
bokelley Aug 29, 2026
8d2bd6f
test(reporting): publish portable reconciliation fixture
bokelley Aug 29, 2026
514ebc7
fix(reporting): start reconciliation before receipt
bokelley Aug 29, 2026
5555949
fix(reporting): pin fixture-bearing source revision
bokelley Aug 29, 2026
615ce33
Merge remote-tracking branch 'origin/main' into protocol-conformance-…
bokelley Aug 29, 2026
104f050
fix(reporting): name generated schema helper types
bokelley Aug 29, 2026
b5ebb24
fix(reporting): repin finalized fixture schemas
bokelley Aug 29, 2026
a4a0d65
fix(reporting): use lifecycle-neutral helper name
bokelley Aug 29, 2026
775a362
fix(reporting): repin neutral generated schema names
bokelley Aug 29, 2026
4b95bb9
fix(reporting): keep codegen names out of prompt projections
bokelley Aug 29, 2026
df3c807
Merge remote-tracking branch 'origin/main' into protocol-conformance-…
bokelley Aug 29, 2026
4185346
test(reporting): repin reconciliation fixture source
bokelley Aug 29, 2026
87e2e42
feat(reporting): enforce the three-tier staging in schemas and confor…
bokelley Aug 30, 2026
b7b665b
docs(reporting): add the reporting.core implementation guide
bokelley Aug 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changeset/add-managed-reporting-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"adcontextprotocol": minor
---

Add the experimental managed reporting delivery surface: caller-owned account
configuration with protocol-managed destination provisioning, `get_reporting_status`
summary/period/revision views, independently scheduled feed purposes, consistent
ledger reconciliation, durable delivery capabilities, immutable
obligation/revision/materialization records, a normative file manifest, authenticated
consumer reconciliation through `sync_reporting_receipts`, and the
`reporting.delivery_ready` notification. Advertise snapshot/official schedules as
atomic offerings, bind their applicability to products, and preserve explicit
full/partial/none/unknown package coverage through configuration, revisions, and
status aggregation. Publish portable, byte-exact reconciliation scenarios for SDKs,
machine-identify required canonicalization vectors, and enforce control-value and
physical-checksum discriminants in the source schemas.

Stage the surface into three conformance tiers over one data model: `reporting.core` (obligations, revisions, materializations, five health states, `get_reporting_status` over existing transports — the only required tier, implementable by a polling-only seller with no destination, manifest, canonicalization, or receipt code), `managed_delivery` (file/dataset-share/warehouse offerings with retention and revocation bounds), and `reconciled_billing` (`sync_reporting_receipts` plus the canonical-digest contract). The `reporting.delivery_ready` doorbell is optional in every tier, and webhook signing is required only when it is declared.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
# have a built-in BOM-strip attribute; rely on editor defaults and review
# to keep them clean.
static/compliance/source/test-vectors/plan-hash/** text eol=lf
static/compliance/source/test-vectors/reporting-reconciliation/** text eol=lf
static/test-vectors/** text eol=lf
1 change: 1 addition & 0 deletions docs/accounts/tasks/list_accounts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ seller-assigned `account_id` or by the complete buyer-declared natural key.
| `account_scope` | How the seller scoped this account: `operator`, `brand`, `operator_brand`, or `agent`. See [account scope](/docs/building/by-layer/L2/accounts-and-agents#account-scope). |
| `payment_terms` | Payment terms agreed for this account: `net_15`, `net_30`, `net_45`, `net_60`, `net_90`, or `prepay`. Binding for all invoices when the account is active. |
| `governance_agents` | Governance agent endpoints registered on this account. Present when governance agents have been configured via [`sync_governance`](/docs/accounts/tasks/sync_governance). |
| `reporting_delivery_configs` | Experimental caller-visible resolved durable reporting configuration states, including setup/readiness, destination binding, immutable generation, and deactivation cutoff. Another caller's configurations are never returned. |
| `setup` | Present when `status: "pending_approval"`. Contains `url` for completing setup and `message` explaining what's needed. |
| `authorization` | Optional. The calling agent's scope grant for this account — `allowed_tasks`, `field_scopes`, `scope_name`, `read_only`. Applies to every vendor agent type (media-buy, signals, governance, creative, brand) — the Accounts Protocol surface is shared. Vendor agents that support scope introspection SHOULD populate this; media-buy sales agents claiming the `attestation_verifier` standard scope MUST populate it. Absence means the vendor agent does not advertise introspectable scope for this account; callers MUST NOT infer access from absence and fall back to error-driven discovery via the RBAC error codes. See [Caller authorization](/docs/accounts/overview#caller-authorization) for the full shape and semantics. |
| `notification_configs` | Account-level webhook subscribers registered via [`sync_accounts`](/docs/accounts/tasks/sync_accounts#account-level-webhook-subscriptions). Each entry carries `subscriber_id`, `url`, `event_types[]`, and `active`. Present when the account has any persisted subscribers. `subscriber_id` is the account-scoped logical key; re-registering the same subscriber replaces that subscriber's config. `authentication.credentials` is omitted on every entry (write-only). Use this surface to verify what's active after a sync, audit fan-out across multiple subscribers, and detect drift between buyer-side expectations and seller-side persisted state. `account.status_changed` subscribers repair by rereading this account; `account.change_recorded` subscribers drain [`list_account_changes`](/docs/accounts/tasks/list_account_changes) and invoke each record's repair task. |
Expand Down
1 change: 1 addition & 0 deletions docs/accounts/tasks/sync_accounts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ asyncio.run(main())
| `payment_terms` | string | No | Payment terms for this account: `net_15`, `net_30`, `net_45`, `net_60`, `net_90`, or `prepay`. The seller must either accept these terms or reject the account — terms are never silently remapped. When omitted, the seller applies its default terms. |
| `sandbox` | boolean | No | When true, set up a sandbox account with no real platform calls or billing. Only applicable to buyer-declared accounts (`require_operator_auth: false`). For account-id namespaces, sandbox accounts are pre-existing test accounts discovered via [`list_accounts`](/docs/accounts/tasks/list_accounts) or supplied out-of-band. |
| `notification_configs` | array | No | Account-level webhook subscribers for events that outlive any single media buy: creative lifecycle notifications, account status changes, and wholesale feed change webhooks. Omit to leave existing subscribers unchanged; send `[]` to remove all subscribers; send a full array to replace. Entries are keyed by account-scoped `subscriber_id`; an existing `subscriber_id` is upserted, and persisted IDs absent from the sent array are removed. |
| `reporting_delivery_configs` | array | No | Experimental caller/account-owned durable reporting desired state. Each entry selects one atomic capability `offering_id` and its exact `report_definition_id`, chooses `delivery_only` or `consumer_receipt` reconciliation, and creates an immutable semantic `(delivery_config_id, delivery_config_version)` generation; only lifecycle intent may change in place. Billing uses `consumer_receipt`. Omit to preserve the caller's set; send `[]` to deactivate it. Destination coordinates are secret-free, and a caller-scoped immutable `destination_ref` may be reused across separately authorized accounts. See [`get_reporting_status`](/docs/media-buy/task-reference/get_reporting_status). |

**Natural key**: The tuple `(brand, operator, operator_unit.id, currency, timezone, sandbox)` identifies the advertiser object in the seller system, with `currency` present only for fixed-currency accounts and `timezone` present only for buyer-selected account-fixed timezones. Within `brand`, only `domain`, `brand_id`, and canonicalized `countries[]` participate. Country order has no semantic meaning; send unique uppercase codes in lexical order for stable keys and signatures. `operator_unit.name` and all mutable BrandRef fields are outside identity, so changing display or per-call metadata does not create a new account.

Expand Down
1 change: 1 addition & 0 deletions docs/building/verification/conformance.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ Every agent MUST pass every storyboard below.
| [`stale_response_advisory`](/compliance/latest/universal/stale-response-advisory.yaml) | [`STALE_RESPONSE`](/docs/building/verification/compliance-catalog#error-code-stale-response) wire placement — advisory rides in `errors[]` on a populated success response with transport success preserved; stale-cache forcing steps gated on `comply_test_controller` with `force_upstream_unavailable` |
| [`idempotency`](/compliance/latest/universal/idempotency.yaml) | `idempotency_key` scoping, replay semantics, [`IDEMPOTENCY_CONFLICT`](/docs/building/verification/compliance-catalog#error-code-idempotency-conflict), `replayed: true`, declared TTL |
| [`read_tool_idempotency`](/compliance/latest/universal/read-tool-idempotency.yaml) | Read-only task wrappers and the 3.x [`get_products`](/docs/media-buy/task-reference/get_products) compatibility facade accept an optional `idempotency_key`; verifies the keyless capabilities bootstrap |
| [`reporting_core`](/compliance/latest/universal/reporting-core.yaml) | The required `reporting.core` tier declaration for [`get_reporting_status`](/docs/media-buy/task-reference/get_reporting_status): tier boundaries enforced in schema (managed_delivery and reconciled_billing are separately advertised; the readiness doorbell and receipt task are never required for Core) |
| [`canonical_format_validate_input`](/compliance/latest/universal/canonical-format-validate-input.yaml) | Canonical-format [`validate_input`](https://adcontextprotocol.org/schemas/v3/creative/validate-input-request.json) result semantics: structural pass/fail across required slots and `unvalidatable_nondeterministic` for seeded product declarations. Gated on agents that advertise `validate_input`; the seeded-product branch also requires `comply_test_controller` seeding support. |
| [`ctv_experience_validate_input`](/compliance/latest/universal/ctv-experience-validate-input.yaml) | [CTV experience profile](/docs/creative/ctv-experiences) `validate_input` semantics: the six `ctv_ad_experience` values against their permitted canonicals, the `overlay`/`squeezeback` duration floor, `in_scene`'s no-interactivity rule, the menu focus/video slot pairing, and non-blocking activation-copy warnings. Gated on agents that advertise `validate_input` and `comply_test_controller` seeding support. |
| [`premium_display_canonical_validation`](/compliance/latest/universal/premium-display-canonical-validation.yaml) | AdCP 3.2 premium-display conformance: bounded state transitions, exact state/breakpoint canvas coverage, coordinated placement/component resolution, namespaced asset coverage, and shared-slot references. |
Expand Down
119 changes: 119 additions & 0 deletions docs/media-buy/reporting-core-implementation-guide.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
title: "Implementing reporting.core"
description: "The required reporting tier: obligations, five health states, and get_reporting_status — over the transports you already have."
"og:title": "AdCP — Implementing reporting.core"
---

<Warning>
Managed reporting is experimental in 3.2. `reporting.core` is its required
tier; `managed_delivery` and `reconciled_billing` are separately advertised
optional tiers.
</Warning>

Every buyer eventually asks one question: **"do I have definitive reporting
for this period — and if not, whose problem is it?"** `reporting.core` makes
that question machine-answerable. It is deliberately small: a seller that
already serves [`get_media_buy_delivery`](/docs/media-buy/task-reference/get_media_buy_delivery)
and fires the existing `reporting_webhook` implements Core **without any
destination, manifest, canonicalization, digest, receipt, or push code**.

## What Core is

Four ideas, one task:

1. **Obligations exist before reports.** For every active delivery
configuration and period, the seller records what *should* exist — so a
missing first report is detectable, not silent.
2. **A zero-row report differs from no report.** An empty period commits a
revision like any other; absence means something is wrong.
3. **Revisions are immutable logical content.** A restatement is a new
revision superseding the old one, never an edit.
4. **[`get_reporting_status`](/docs/media-buy/task-reference/get_reporting_status)
answers "where am I?"** — one authoritative read over the obligation
ledger, summarized by five health states.

| Health | Meaning |
|---|---|
| `healthy` | Everything due has been produced. |
| `waiting` | Nothing is due yet. |
| `delayed` | Something due is late, automated recovery is still running. |
| `action_required` | A human on the named `responsible_party` must act. |
| `complete` | The queried scope is closed and every final obligation is satisfied. |

## What Core is not

- **No push requirement.** Polling `get_reporting_status` is the
authoritative recovery path; a polling-only seller is fully conformant.
If you do offer push, Core's doorbell is `reporting.status_changed` — an
invalidation for health transitions in either direction, with stable
`issue_ids`. `reporting.delivery_ready` belongs to the managed-delivery
tier and never fires for Core configurations.
- **No delivery machinery.** Core offerings omit `method`: rows flow through
`get_media_buy_delivery` and the `reporting_webhook` you already serve.
Destinations, manifests, and provisioning belong to the `managed_delivery`
tier.
- **No canonical hashing or receipts.** `reconciliation_mode` is
`delivery_only`, offering profiles omit the `canonicalization_*` contract,
and `sync_reporting_receipts` does not exist for you. Those belong to the
`reconciled_billing` tier.

The boundary is executable: the `reporting-core` fixture test in the
protocol repository implements a complete polling-only seller and asserts
its source contains none of
`destination | manifest | canonical | digest | receipt | readiness | webhook`.

## Build order for a seller

1. **Advertise the tier.** In `get_adcp_capabilities`, add
`media_buy.reporting_delivery` with `supported: true`, `configuration_task`,
`status_task`, at least one offering, `automated_recovery_window_seconds`,
and `status_retention_days` — and add `media_buy.reporting_delivery` to
`experimental_features`. Do not set `managed_delivery` or
`reconciled_billing` until you implement them.
2. **Define one Core offering.** Feed purpose, an immutable
`report_definition_id`, a content-addressed reporting profile (schema URI
+ SHA-256, grain, primary keys), a schedule (`period_duration`,
`alignment`, `delivery_sla`), supported finality, and
`reconciliation_mode: "delivery_only"`. No `method`.
3. **Generate obligations.** When an account installs a delivery
configuration through [`sync_accounts`](/docs/accounts/tasks/sync_accounts),
derive one obligation per period from the schedule and the frozen
media-buy scope. Campaign starts, stops, and configuration generations
change *future* obligations, never past ones.
4. **Commit revisions.** When a period's data is final enough for its
declared finality, record an immutable revision — including zero-row
periods. Restatements create superseding revisions.
5. **Project status.** Serve `get_reporting_status`: summary and periods
views over a consistent ledger snapshot, health computed from obligations
versus revisions versus the clock, `issues[]` carrying the typed code,
`responsible_party`, and `recommended_action` whenever health is
`delayed` or `action_required`.
6. **Escalate honestly.** A due obligation stays `delayed` while automated
recovery runs, for at most `automated_recovery_window_seconds`; after
that it is `action_required`. Never park a dead feed in `delayed`.

## How to verify

- The **`reporting-core` universal storyboard** grades your capability
declaration and tier boundaries.
- The **reporting-core fixture test** is the tier boundary stated as code —
copy its Core capability block and offering as your starting fixtures.
- Buyers verify you the same way their SDKs do: webhook or not, they poll
`get_reporting_status` and reconcile obligations against what they
received.

## The ladder above Core

Nothing in Core obligates you to climb, and each rung is separately
advertised:

- **`managed_delivery: true`** adds offerings with a `method` —
file transfer, dataset shares, warehouse materialization — plus
exact-resource retention and authorization-revocation bounds, bound to
principal-layer destinations ([`sync_principal`](/docs/protocol/sync_principal)).
- **`reconciled_billing: true`** adds `receipt_task`
([`sync_reporting_receipts`](/docs/media-buy/task-reference/sync_reporting_receipts)),
`consumer_receipt` reconciliation, and the canonical-digest contract —
bilateral agreement on exact logical content, for billing.

Start with Core. It solves the problem everyone actually has.
Loading
Loading