Managed reporting is experimental in 3.2. reporting.core is its required
tier; managed_delivery and reconciled_billing are separately advertised
optional tiers.
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
can add Core while any existing reporting webhooks continue unchanged. Core
itself requires no destination, manifest, canonicalization, digest, receipt,
or push code.
What Core is
Four ideas, one task:
- 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.
- A zero-row report differs from no report. An empty period commits a
revision like any other; absence means something is wrong.
- Revisions are immutable logical content. A restatement is a new
revision superseding the old one, never an edit.
get_reporting_status
answers “where am I?” — one authoritative read over the obligation
ledger, summarized by five health states.
Lifecycle at a glance
The reporting configuration creates the clock. Accepting a media buy does not
start a separate reporting SLA. Before a period closes, both sides can derive
its boundary and next_expected_at, but the seller cannot freeze an
all_media_buys denominator yet. At the period boundary, the obligation
becomes part of the authoritative ledger whether or not source data exists.
This gives each side a distinct responsibility:
- Seller: commit every elapsed eligible period before taking a ledger
snapshot after
period.end, independently of source availability and before
committing a revision. A matching periods view includes it in the
paginated record set.
- Buyer: retain the accepted configuration generation, derive the same
expected periods, and treat a missing obligation as a protocol failure rather
than evidence that no activity occurred.
- Both: calculate
expected_at as period.end + delivery_sla. The
obligation is committed at period close and appears in the first ledger
snapshot strictly after it; the report becomes late only at
expected_at.
For example, a configuration activated at 00:20Z with hourly aligned periods
starts at the next full boundary. Its first period is [01:00Z, 02:00Z). The
obligation appears in the first ledger snapshot strictly after 02:00Z; with
delivery_sla: PT1H, it stays waiting through 03:00Z and becomes
delayed after that if no revision has been produced. A snapshot exactly at
02:00Z does not expose it.
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: the status ledger
and its immutable revision metadata are required, while any existing
get_media_buy_delivery read or legacy reporting webhook remains an
optional data transport. Core never requires push. 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
- 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.
- 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.
- Generate obligations. When an account installs a delivery
configuration through
sync_accounts,
derive its future period boundaries and next_expected_at. At each eligible
period.end, freeze the media-buy scope and commit that obligation
independently of source availability and before committing its first
revision. Every later ledger snapshot must record it, and a
matching get_reporting_status periods view must return it, even when the
first report never arrives. Campaign starts, stops, and configuration
generations change future obligations, never past ones.
- 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.
- 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.
- 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.
Build order for a buyer
- Save the contract. Persist every accepted reporting configuration
generation, including its schedule, activation boundary, scope, report
definition, coverage requirement, and required finality. Do not reconstruct
historical expectations from a seller’s current configuration.
- Derive expected periods. Calculate the same half-open intervals and
expected_at timestamps. A mid-period configuration activation begins at
the next full boundary; deactivation still owes a period that already
started.
- Use your own denominator. For each elapsed expected period, require a
matching obligation in the complete paginated
periods view. The seller’s
returned obligation list cannot prove that the first one was not omitted.
- Close the history. Keep one
ledger_snapshot_id across every page,
deduplicate immutable IDs, match the returned record counts, and distinguish
a zero-row revision from no revision. Apply managed-resource and receipt
checks only when the selected tier requires them.
- Treat push as a wake-up. A doorbell starts a status read; it never
substitutes for one. Poll periodically even when notifications are enabled.
How to verify
- The
reporting-core-declaration universal storyboard grades capability
and tier boundaries without a controller. The lifecycle-focused
reporting-core storyboard uses reporting_core_lifecycle_probe to
verify an obligation-before-report, clock-derived delayed health, and
explicit zero-row publication.
- 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 reporting lab requires a negotiated AdCP 3.2 release that supports
get_reporting_status (the current training surface is 3.2-beta.11); the
training agent otherwise serves its default 3.0 surface, where these
experimental reporting fields are unavailable. Put the same negotiated version
on every sync_accounts, controller, and get_reporting_status request.
In the public sales sandbox, call comply_test_controller with
scenario: "reporting_core_lifecycle_probe" and operation: "prepare". Use
the returned account and stable identifiers in a get_reporting_status
periods read. Then run advance_time with target_health: "delayed", read
the summary, run publish_zero_row, and read the periods view again. The
controller reference
contains the exact request shapes.
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).
reconciled_billing: true adds receipt_task
(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.