Skip to content

RFC: reliable reporting status and managed delivery #6911

Description

@bokelley

Status (2026-08-30): staged for ratification — one data model, three conformance tiers. The architecture below stands, but conformance splits into tiers and only Core is required. PR #6953 is draft until the merge gates hold.

Staging decision

Implementation review (5,295-line protocol PR; ~11k and ~15k-line SDK prototypes) showed this RFC had grown into three products. The model is preserved; conformance is tiered:

Tier Capability Contains Required?
Core reporting.core Obligations, revisions, materializations, five health states, get_reporting_status — over transports sellers already have (get_media_buy_delivery, existing reporting_webhook). The reporting.delivery_ready doorbell is optional: a polling-only seller is fully Core-conformant. Yes
Managed delivery reporting.managed_delivery Files, dataset shares, warehouses, manifests — binding to principal-layer destinations (#6966 / #7015) No
Reconciled billing reporting.reconciled_billing Canonical digest, consumer receipts, sync_reporting_receipts No

Core alone solves the original problem — "where is yesterday's reporting, and is anything missing?" — and a basic webhook/polling seller implements it without understanding destinations, manifests, canonical hashing, or receipts.

Merge gates for #6953:

  1. Proposal: unify operational snapshots and official delivery as revisioned facts #6122's finality model receives WG agreement (critical path).
  2. Destination/verification vocabulary consolidated onto principal-layer $refs from feat(protocol): add agent configuration sync #6966.
  3. An independent human protocol review approves the scope.
  4. A minimal Core fixture demonstrates a polling-only seller passing reporting.core conformance with no destination, manifest, hashing, or receipt code.

Product goal

AdCP should make reliable reporting feel like one product surface:

A consumer asks “do I have definitive reporting for this period?”, gets a machine-checkable answer, and only involves a human when production, delivery, or reconciliation cannot recover automatically.

Moving bytes is not enough. The contract must distinguish:

  1. what reports were supposed to exist;
  2. what immutable logical content the seller published;
  3. where each publication was materialized; and
  4. whether each intended consumer independently observed matching data.

The SDK should orchestrate those facts. Applications should not build their own reporting state machine.

Minimal protocol surface

Reuse configuration and notifications

  • get_adcp_capabilities advertises atomic managed-reporting offerings.
  • sync_accounts.reporting_delivery_configs[] selects and authorizes independently scheduled pacing, analytics, and billing feeds for one account.
  • reporting.delivery_ready is registered through existing account notification configuration and acts only as a signed, secret-free doorbell.
  • Existing reporting_webhook and get_media_buy_delivery remain the low-latency and compatibility data APIs.

The authenticated transport principal is the buyer/governance agent identity. It is not repeated as an untrusted payload field.

Agent-scoped reusable destinations are complementary, not a replacement for account configuration. The separate sync_agent_configuration proposal in #6966 lets a caller prove and register one BigQuery dataset, S3 prefix, Snowflake account, or Databricks recipient once for its relationship with a seller. Each account-level reporting configuration still authorizes which account, feed, media-buy scope, and schedule may use the resulting seller-issued destination reference. This allows consolidation across many accounts without making destination possession equivalent to data access.

Add one authoritative read task: get_reporting_status

The task has explicit views:

  • summary: operational health for an exact account/feed/time denominator;
  • periods: a stable cursor-paginated ledger used for complete reconciliation; and
  • revision: exact retained revision/resource resolution.

The five health states remain simple:

State Meaning
healthy Due obligations in an open scope are current and automatic delivery is working.
waiting Nothing in the selected open scope is due yet.
delayed Something is late but still inside automatic recovery.
action_required An SLA/retry boundary was crossed and a structured responsible party/action is available.
complete The scope is closed, retained, and every obligation satisfies finality, delivery, and any required consumer reconciliation.

Add one narrow write task: sync_reporting_receipts

SDK prototyping confirmed that the seller cannot infer consumer agreement from successful publication. This is especially clear for a buyer-owned destination: the seller may be allowed to write but correctly forbidden from reading it.

sync_reporting_receipts therefore records an authenticated consumer's accepted or rejected observation of one exact materialization. It is not a webhook acknowledgement. An accepted receipt includes row count and profile-defined control totals plus the selected verification evidence. A rejected receipt records stable mismatch codes. The consumer then reads the receipt back through get_reporting_status.

This is the minimum back-and-forth needed for the seller to know “the buyer says it has the same numbers.” It is still an attestation, not cryptographic proof that the buyer used those rows correctly after its stated downstream commit.

A replay mutation remains deferred. Retained immutable resources and the authoritative ledger provide ordinary missed-event recovery.

Capability declaration, snapshots, and mixed-source coverage

Managed delivery advertises exact reporting products, not a generic promise that a transport can carry every report.

  • The seller-wide media_buy.reporting_delivery.offerings[] catalog declares atomic combinations of report definition and row grain, feed purpose, cadence/alignment/SLA, supported snapshot and/or official finality, reconciliation profile, and delivery method. Hourly snapshots to object storage and daily official billing through a warehouse share are separate offerings.
  • Product.reporting_capabilities.reporting_delivery_offering_ids declares which seller-wide offerings packages using that product can satisfy. An empty list means none; omission means unknown, not universal support.
  • Account, seat, credential, provider, and API constraints may narrow eligibility during sync_accounts.
  • Each period obligation freezes the resulting media-buy and package coverage so later capability changes do not rewrite history.

A configuration selects coverage_requirement: full | allow_partial. Coverage is returned independently as full | partial | none | unknown, together with exact covered, unsupported, and unknown package/media-buy sets. A fresh, successfully delivered partial snapshot remains partial. Covered-subset metrics MUST NOT be presented as complete whole-buy totals, and implementations MUST NOT silently weaken the selected profile to a lowest common denominator.

Event-level or exposure-log access is a distinct reporting profile with its own privacy and authorization requirements. Advertising a clean-room or warehouse transport does not imply that profile is available.

Reliability data model

Obligation

One destination/configuration-specific report that should exist for an exact period, scope, finality, and due time. Obligations exist before delivery, which makes a missing first webhook or missing first file detectable.

Revision

One immutable logical publication. Revisions are deliberately destination-independent. They contain the report-definition/profile identity, account and media-buy scope, exact period, finality, observation/data-through boundary, row count, control totals, optional canonical content digest, and supersession lineage.

One canonical revision can fan out to S3, BigQuery, Databricks, Snowflake, a buyer agent, and a governance consumer without acquiring a different logical identity. A restatement creates a new revision and points to the immediately superseded revision. A zero-row revision is real and distinct from no revision.

Materialization

One attempt to expose a revision for one obligation and destination. Delivery configuration, destination, method, retry attempt, resource descriptor, and producer-side verification belong here. A retry creates a new materialization while preserving revision identity.

Receipt

One authenticated consumer's observation of one exact materialization. Buyer and governance consumers receipt independently; neither principal's result implies the other's.

Definitive-period algorithm

A buyer does not establish completeness by trusting one seller boolean. An SDK must:

  1. derive the exact expected periods from its saved delivery-configuration generations and resolved calendar boundaries;
  2. request that account/config/feed/time scope;
  3. require scope_closed: true and full retained-ledger coverage;
  4. exhaust a cursor bound to one immutable ledger_snapshot_id and ledger_as_of;
  5. deduplicate obligation, revision, materialization, and receipt records by immutable IDs;
  6. require the unique record count to equal pagination.total_count;
  7. require each obligation's declared revision/materialization/receipt counts to equal the associated records actually returned;
  8. require one obligation for every locally expected period;
  9. select exactly one current revision and require the configured finality;
  10. require a verified, unexpired exact materialization; and
  11. when configured for consumer reconciliation, require an accepted matching receipt for that current revision/materialization.

This answers “do I have all of yesterday?” even when the first report never existed. For an open hour or month, the same surface reports current health and conservative data_through; it does not call an unfinished scope complete.

Totals are aggregated once per canonical revision, not once per destination, so fan-out cannot double-count spend or impressions.

Verification profiles

Full canonical hashing is valuable but too expensive to require for every frequent snapshot. Offerings select one explicit profile:

  • native_commit: immutable provider-native version/transaction evidence plus row count and control totals;
  • manifest_checksums: a digest-valid manifest plus every listed object's cryptographic checksum, row count, and control totals; or
  • canonical_digest: a digest of canonical logical rows under a content-addressed canonicalization contract.

Only canonical_digest claims exact logical equality across differently encoded/materialized copies. Billing requires official finality, canonical_digest, reconciliation_mode: consumer_receipt, and an accepted current receipt. Pacing and analytics may use cheaper profiles when the selected atomic offering allows them.

Normative file delivery

file_transfer uses a manifest-last commit. Producers write all immutable data objects first and publish the manifest last. Consumers ignore unlisted objects and do not process a materialization until the exact manifest digest validates.

The v1 manifest binds:

  • revision, obligation, materialization, and period identity;
  • format and compression;
  • the complete object list;
  • per-object sizes, row counts, and SHA-256/SHA-512 checksums;
  • total size and row count;
  • profile-defined control totals; and
  • creation time and complete: true.

This is the interoperability contract for S3/GCS/Azure-style delivery, rather than prose saying only “manifest last.”

Destination and orchestration semantics

The open method patterns are:

Pattern Protocol boundary
file_transfer A complete immutable manifest and all listed objects are readable and verified.
dataset_share A versioned relation/share is readable through a representative intended consumer path.
warehouse_materialization The exact revision is committed in the configured destination with the selected verification evidence.

producer_managed versus consumer_managed says who orchestrates delivery. It does not say who owns the table or which platform service writes it. GAM BigQuery Data Transfer can be consumer-managed even though the transfer service performs the write. A buyer-owned destination may end the seller's technical visibility at verified commit; the receipt reports the consumer's independent observation and optional non-secret downstream checkpoint.

Credentials, signed URLs, bearer profiles, passwords, and private keys never transit AdCP. Provider-native grants or explicit human setup handle access.

Fast path and reliable path

  • A short-period snapshot pacing feed gives the fast path. reporting.delivery_ready wakes the consumer quickly; polling repairs missed events.
  • Daily or billing-cycle official feeds give the slower authoritative path with longer SLA and retention.
  • Buyers may install pacing, analytics, and billing configurations simultaneously and reconcile each independently.
  • Event-level exposure logs are deferred until AdCP defines their privacy, authorization, purpose, minimization, retention, and recipient contract.

Campaign-control decisions based on feed health belong to buyer policy or Campaign Pulse. This reporting RFC exposes reliable evidence and never pauses or resumes spend itself.

Live and SDK validation

Live synthetic validation has exercised the two less-obvious destination paths:

  • BigQuery warehouse materialization: immutable A/B revisions, duplicate-safe commit, retained A after restatement B, exact replay, row-count and digest agreement.
  • Databricks Open Sharing: recipient-observed readiness, retained historical version reads, zero rows, revoke/regrant, and exact replay. The roughly 15-second recipient visibility delay proved that provider-side grant completion is not readiness. Open-client incompatibility with Delta Deletion Vectors proved that reader compatibility belongs in the offering.

The proposed schemas were then generated into current local checkouts of both SDKs:

  • JavaScript build and strict typecheck pass. Four end-to-end reconciliation tests pass: receipt write/readback with transient destination retry, missing expected-period detection, incomplete associated-history detection, and destination fan-out without double-counting.
  • Python Ruff and strict MyPy pass for the helper, and the same four reconciliation tests pass.
  • Python generation also exposed SDK work that must be fixed in its own PR: some current protocol models have renamed generated types still referenced by hand-written aliases, and canonical macro references need corrected $ref rebasing.

S3 uses standard object-store mechanics and is not a remaining schema-design gate, though a small live smoke test should become a conformance fixture before this experimental feature graduates.

Implementation

The first protocol/schema implementation is #6953. It includes the status and receipt tasks, four-record ledger, destination-independent revisions, normative manifest, verification profiles, delivery configuration/capabilities, signed readiness notification, security constraints, and contract tests.

The feature remains experimental while we gain multi-party production experience. The immediate follow-on is to land clean JavaScript and Python SDK PRs around the validated helper behavior, then exercise a seller adapter and an independent consumer/governance adapter end to end.

Metadata

Metadata

Assignees

No one assigned

    Labels

    claude-triagedIssue has been triaged by the Claude Code triage routine. Remove to re-triage.media-buyIssue concerns the media-buy protocol domainneeds-wg-reviewBlocked on a working-group decision — surface in WG meeting agendasrfcProtocol change — auto-adds to roadmap boardspec / protocol

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions