Reliable 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. Core is the required operational reporting
baseline: a seller must retain immutable revision evidence and expose its exact
binding over its existing reporting transports. Core requires no destination,
manifest, external materialization, receipt, or push code. It does require the
fixed revision-binding digest over canonical rows; this is distinct from the
Managed Delivery external-materialization canonicalization contract. Core carries
the operational cost of durable obligations, revision retention, and repair.
For an exact Core revision read, call get_media_buy_delivery with
reporting_revision_id and, optionally, an account selector. The returned reporting_revision_binding
must echo that ID and bind { reporting_revision_id, row_count, control_totals, reporting_rows } with RFC 8785 JCS and SHA-256. Exact reads are cursor-paginated:
every page repeats identical revision metadata and binding, and a consumer hashes
the concatenated rows in cursor order only after exhausting the frozen walk. Do
not substitute a fresh date-range pull for this read: it may have changed since
the immutable revision was published.
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 provisional restatement is a
new snapshot revision superseding the old snapshot, never an edit. An
official revision is immutable billing-purpose evidence and cannot be
superseded; later corrections are explicit accounting adjustments.
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.ledger_changed is the separate optional invalidation
for every new revision or adjustment, including changes that leave health
untouched. 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 Managed canonicalization or receipts.
reconciliation_mode is
delivery_only, offering profiles omit the external-materialization
canonicalization_* contract, and sync_reporting_receipts does not exist
for you. Core still commits its fixed revision-binding JCS/SHA-256 digest;
receipts and Managed external materialization machinery 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 | receipt | readiness | webhook (the Core binding
digest is intentionally retained).
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. Snapshot restatements create superseding revisions. Official
revisions are terminal; publish any later source correction as a signed
delta in a
reporting-adjustment record applied to an open accounting
period.
- 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. An external billing system MAY retain
an exact official reporting_revision_id as supporting evidence and apply
later adjustments under its own controls. Apply managed-resource and receipt checks only when the selected
tier requires them.
- Checkpoint repair. After consuming every page, persist
changes_checkpoint; send it as changes_after on the next periods read.
Deduplicate safely replayed records by immutable ID.
- Treat push as a wake-up.
reporting.ledger_changed starts a
changes_after repair. Because a clock-only health transition may commit no
ledger record, reporting.status_changed starts a non-incremental current
summary or periods read. Poll periodically even when notifications are enabled.
How to verify
- The
reporting-core-declaration universal storyboard grades the
proper-name Reliable Reporting 1.0 declaration and Core boundary. The
capability-gated reliable-reporting-managed-delivery and
reliable-reporting-reconciled-billing storyboards grade the optional
tiers. 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
Reliable Reporting 1.0 (planned for the next RC.1 wire bundle); the deployed
training agent otherwise serves its RC.0/default 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.
File-transfer object and version references
For Managed Delivery file transfers, every manifest files[] entry carries a
credential-free, destination-relative object_ref. For an S3 destination,
that means the decoded object key relative to the configured destination — not
an s3:// URI and not a key with ?versionId= appended. The object key has a
1,024-character schema bound, which accommodates S3’s 1,024-UTF-8-byte key
limit; JSON Schema measures characters, not bytes.
When a provider has an immutable object version, place its decoded native
value in the entry’s optional native_version_ref, separately from the key.
This covers S3 VersionIds as well as GCS generations and provider-native
snapshots. A VersionId can include characters such as +, /, and = that
expand when URI encoded. Keep the decoded value in the manifest and encode it
only while constructing the provider request. The same destination-relative
object_ref is used by physical_checksums[], so it identifies the same
object without carrying credentials, signed URLs, or query parameters.
reporting-file-manifest.json contains the files[] entries:
reporting-verification.json contains the physical_checksums[] entries: