Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions .changeset/reporting-subscriptions-clarify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"adcontextprotocol": minor
---

Settle the reporting-subscription ambiguities the TypeScript and Python SDK implementations hit. Each rule below is what both SDKs now compute.

- **Mapped dimensions on the wire.** Offerings gain experimental `standard_dimensions` (the breakdown in the standard vocabulary) and `unmapped_dimensions` (native dimensions with no mapping), so a buyer resolves `match` from the wire exactly as the seller does. Sellers advertising `media_buy.reporting_subscriptions` MUST publish `standard_dimensions` on every offering. The documented worst-case capabilities size moves to 91,804,421 bytes.
- **`match: exact` without `dimensions`.** Every candidate qualifies and the lowest `offering_id` wins.
- **`period_timezone`.** An `account_timezone` offering uses it in place of the account's zone. A `utc` offering matches only a UTC alias. A `source_timezone` or fixed `billing_cycle` offering matches only its own published zone. A buyer-configured `billing_cycle` offering never matches a subscription.
- **Period arithmetic.** Years, months, and days are civil time. Hours, minutes, and seconds are exact elapsed time, so `PT24H` differs from `P1D` across DST. Zero-length periods are never owed.
- **Cutovers across different offerings.** E is computed per incoming offering: a continuing offering hands off at its own boundary, and a new one starts at the latest outgoing end.
- **Authorization chains.** `authorized_at` is the start of the current authorization chain. A re-grant within `status_retention_days` continues the chain, so withheld periods stay owed; a later re-grant starts a new one.
- **Reactivation.** Turning an inactive subscription back on is a fresh start. The readback lists `inactive_intervals` so a buyer can recompute after losing local state.
- **`scope_resolved_at`.** For subscription obligations it is the period end.
- **Per-offering exceptions.** `gaps[]` and `timezone_cutovers[]` entries can carry `offering_id` when offerings' boundaries differ, and timezone cutovers carry `previous_timezone`.
2 changes: 1 addition & 1 deletion docs/building/by-layer/L1/security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2557,7 +2557,7 @@ schema-valid. The annotations are registered as a 4.0 tightening in the
[4.0 compatibility checks](/docs/reference/migration/v4-compatibility#capabilities-stay-within-their-structural-bounds).

**Worst case.** With the 4.0 bounds applied, the largest schema-valid
capabilities document is 91,262,597 bytes (about 87 MiB), counting only the
capabilities document is 91,804,421 bytes (about 88 MiB), counting only the
members the schema declares. `scripts/capabilities-structural-bounds.cjs` computes it
and `tests/capabilities-structural-bounds.test.cjs` pins it, so the figure moves
only when a limit moves. The calculation assumes every declared member is
Expand Down
27 changes: 21 additions & 6 deletions docs/media-buy/task-reference/get_reporting_status.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ periods that:
- the subscription version's `accepted_at`, in the
[`get_principal`](/docs/protocol/get_principal) readback;
- the buy's `confirmed_at`, when the seller committed to it; and
- the account's `authorization.authorized_at`, when the current grant began; and
- the account's `authorization.authorized_at`, when the current authorization chain began. A re-grant within `status_retention_days` of losing authority continues the chain and leaves it unchanged; and
2. overlap the buy's flight, from its `start_time` to its **stop time**. The stop time
is the earliest of `end_time`, `cancellation.canceled_at`, and `completed_at`,
which sellers advertising `media_buy.reporting_subscriptions` populate for every
Expand All @@ -689,8 +689,8 @@ A buy canceled mid-period owes that period and none after it. A buy confirmed on
November 1 with a December 1 start owes nothing before December 1. Every buy has an
`end_time`, so there is no open-ended case.

Flight values are taken as of each period's `scope_resolved_at`, matching how each
obligation freezes its `media_buy_ids`. An
Flight values are taken as of each period's `scope_resolved_at`, which for a subscription
obligation is the period end, matching how each obligation freezes its `media_buy_ids`. An
[`update_media_buy`](/docs/media-buy/task-reference/update_media_buy) extension adds
only periods not yet resolved. Reinstating or extending a buy after a period has
elapsed never makes that period owed retroactively.
Expand Down Expand Up @@ -732,9 +732,20 @@ owed.
- **Resolution change.** If the seller adds an offering that changes the resolution, the
old offering is outgoing and the new one incoming, so the change never touches a
period already started.
- **Different offerings on each side.** With `match: all`, or a resolution change, the
outgoing and incoming sides can resolve different offerings. E is then computed per
incoming offering: an offering present on both sides hands off at the end of its own
last period that started before T; a newly resolved offering starts at the latest
such end across the outgoing offerings.
- **Reactivation.** Turning an inactive subscription back on is a fresh start at the
reactivation instant, under the ordinary start rule. Nothing that starts while the
subscription was inactive is owed. The readback lists each interval in
`inactive_intervals`, so a buyer can recompute after losing local state.
- **Account timezone change.** For account-timezone periods, the old timezone's
schedule is outgoing. The E instant is listed as `effective_at` in
`timezone_cutovers[]`, and `list_accounts` shows the new `timezone` and `revision`.
`timezone_cutovers[]`, with the old zone as `previous_timezone`, and `list_accounts`
shows the new `timezone` and `revision`. When the account's offerings have different
boundaries, the seller lists one entry per `offering_id`.

Each obligation names the `offering_id` and `report_definition_id` it used, and that
offering is authoritative for the obligation's timing: its alignment, anchor, and
Expand Down Expand Up @@ -826,13 +837,17 @@ data.
statuses for revisions the caller read before `unauthorized_at`. Every other
reference to the account is unavailable.
- **Re-grant.** The account leaves `accounts_unauthorized[]` and is readable here
again. Withheld periods are owed again, due at the re-grant time plus the offering's
again. The re-grant continues the earlier authorization chain, so
`authorization.authorized_at` keeps its original value. Withheld periods are owed again, due at the re-grant time plus the offering's
`delivery_sla` rather than their original `expected_at`. With
`subscriptions.backfill_on_regrant: true`, no gap is recorded and every period in
the revoked interval is owed, on the same re-grant deadline. Otherwise the interval
is recorded in `gaps[]` with reason `unauthorized`, `from` the last boundary at or
before `unauthorized_at` `until` the first boundary at or after the re-grant, and its
periods are not owed.
periods are not owed. Boundaries differ between offerings, so when the account's
offerings have different boundaries the seller records one gap per `offering_id`,
each with that offering's boundaries; a gap without `offering_id` applies to every
offering.
- **Expired entry.** After `status_retention_days` the `accounts_unauthorized[]` entry
drops. A later re-grant is then a fresh coverage start from the new `authorized_at`,
with no gap claimed.
Expand Down
12 changes: 7 additions & 5 deletions docs/protocol/sync_principal.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -323,14 +323,16 @@ An account is covered while the scope selects it and the principal is authorized
| `required_finality` | `snapshot` or `official`, which the offering must list in `supported_finality`. |
| `alignment` | Optional. Only offerings whose schedule uses this alignment (`utc`, `account_timezone`, `source_timezone`, `billing_cycle`) are candidates. |
| `dimensions` | Optional breakdown in the standard vocabulary: `package`, `creative`, `country`, `region`, `metro`. The vocabulary is extensible: an unknown value matches no offering. |
| `match` | `exact` picks the offering whose dimension set equals `dimensions`. Native dimensions that don't map to the vocabulary count as distinct, so `package × device` does not equal `package`. `finest` picks the offering with the most mapped dimensions among those that include `dimensions`. Ties under `exact` and `finest` go to the lowest `offering_id`. `all` picks every offering that includes `dimensions`, each with its own obligations. Defaults to `exact` when `dimensions` is present and `finest` when it is absent. Billing subscriptions SHOULD use `exact`. |
| `period_timezone` | Optional IANA timezone override, only when `account_ids` names exactly one account. |
| `match` | `exact` picks the offering whose dimension set equals `dimensions`. Native dimensions that don't map to the vocabulary count as distinct, so `package × device` does not equal `package`. `finest` picks the offering with the most mapped dimensions among those that include `dimensions`. Ties under `exact` and `finest` go to the lowest `offering_id`. `all` picks every offering that includes `dimensions`, each with its own obligations. Defaults to `exact` when `dimensions` is present and `finest` when it is absent. An explicit `exact` with no `dimensions` lets every candidate qualify, and the lowest `offering_id` wins. Billing subscriptions SHOULD use `exact`. |
| `period_timezone` | Optional IANA timezone override, only when `account_ids` names exactly one account. An `account_timezone` offering uses it in place of the account's zone. A `utc` offering matches only a UTC alias (`UTC`, `Etc/UTC`). A `source_timezone` or fixed `billing_cycle` offering matches only when its published `period_timezone` is the same zone. |

Each offering publishes its breakdown in the standard vocabulary as `standard_dimensions`, and any native dimensions it can't map as `unmapped_dimensions`, so a buyer resolves `match` from the wire exactly as the seller does. Sellers advertising subscriptions MUST publish `standard_dimensions` on every offering, empty for totals-only offerings. A `billing_cycle` offering whose anchor the buyer configures never matches a subscription, because a subscription can't supply the anchor.

For example, if a seller offers `package`, `package × creative`, and `package × country` breakdowns, `dimensions: ["package"]` resolves to the `package` offering under the default `exact`; `finest` would pick one of the two finer offerings by `offering_id`, and `all` would pick all three.

`reconciliation_mode` must also match the offering. Without a `destination` the subscription selects only API-delivered (Core) offerings; with one, only offerings whose `method` matches that destination's pattern and transport. Billing subscriptions require `official` finality and `consumer_receipt`, and `consumer_receipt` requires a destination. The seller resolves the selector separately for each account and each buy's products. Each (buy, resolved offering) pair is owed its own periods, and each obligation names its `offering_id` and `report_definition_id`. An account or buy with no matching offering gets an `offering_unsupported` exception. When only some of a buy's packages resolve, the buy is covered for those, and the exception names the uncovered `package_ids`. Neither case fails the request or the subscription.

**Period boundaries.** The resolved offering's published schedule governs period boundaries: its alignment (account timezone, UTC, an upstream source timezone, or a billing cycle), anchor, and timezone, which the buyer reads from the offering. Use the selector's `alignment` to require one. `period_timezone` overrides the timezone for a subscription scoped to exactly one account. When an account's timezone changes, for example through a [`request_account`](/docs/accounts/tasks/request_account) change, account-timezone periods switch at the first old-timezone boundary at or after the change. The switch is recorded in `get_reporting_status` as a `timezone_cutovers` entry, and [`list_accounts`](/docs/accounts/tasks/list_accounts) shows the new `timezone` and `revision`.
**Period boundaries.** The resolved offering's published schedule governs period boundaries: its alignment (account timezone, UTC, an upstream source timezone, or a billing cycle), anchor, and timezone, which the buyer reads from the offering. Use the selector's `alignment` to require one. `period_timezone` overrides the timezone for a subscription scoped to exactly one account. Years, months, and days are civil time in that timezone; hours, minutes, and seconds are exact elapsed time, so `PT24H` and `P1D` differ across a DST change. When an account's timezone changes, for example through a [`request_account`](/docs/accounts/tasks/request_account) change, account-timezone periods switch at the first old-timezone boundary at or after the change. The switch is recorded in `get_reporting_status` as a `timezone_cutovers` entry, and [`list_accounts`](/docs/accounts/tasks/list_accounts) shows the new `timezone` and `revision`.

**Destinations.** `destination.destination_id` names an entry in this principal's `reporting_destinations`, which must still exist after the same request applies. Every delivered row carries `account_id`. A `path_template` is a relative path with no `.` or `..` segments; allowed placeholders are `{account_id}`, `{subscription_id}`, `{offering_id}`, and `{period_start}`. Each substituted value fills exactly one path segment: the seller percent-encodes (RFC 3986) any value that contains `/` or equals `.` or `..`. It must contain `{account_id}` when the scope can cover more than one account, and is then required for `file_transfer` destinations. When more than one active subscription of the principal shares a destination, every template must contain `{account_id}` and the templates must differ. When an agency must keep advertisers apart, an account-scoped subscription points that advertiser's feed at its own destination; sellers that support subscriptions MUST support that.

Expand All @@ -341,7 +343,7 @@ For example, if a seller offers `package`, `package × creative`, and `package

A buy confirmed on November 1 with a December 1 start owes nothing before December 1. Flight values are taken as of each period's `scope_resolved_at`. So an [`update_media_buy`](/docs/media-buy/task-reference/update_media_buy) extension adds only periods not yet resolved, and reinstating or extending a buy never makes an elapsed period owed after the fact. The period containing the stop time is owed in full. Timing (`expected_at`) comes from each obligation's offering. The seller reports only exceptions, and each removes periods; none adds one. See [`get_reporting_status`](/docs/media-buy/task-reference/get_reporting_status#which-periods-are-owed).

**Versions and cutovers.** A new `subscription_version` is required whenever delivery semantics or scope change: the selector, `reconciliation_mode`, destination, `scope`, `exclude_account_ids`, or `operator_domains`. Each version has its own `accepted_at`. `active` and `replace_account_configs` change in place. Changes are **cutovers** at the instant they take effect, T. The outgoing schedule owes its periods that start before T, in full, ending at E. The incoming schedule owes its periods that start at or after E. No period is owed twice. When the two schedules' boundaries don't align, the stretch from E to the incoming schedule's next boundary is owed under neither; it is shorter than one incoming period. Version changes (for accounts in both versions, T is the new `accepted_at`), deactivation, overrides, `replace_account_configs`, offering resolution changes, and account timezone changes all use this rule. See [cutovers](/docs/media-buy/task-reference/get_reporting_status#which-periods-are-owed).
**Versions and cutovers.** A new `subscription_version` is required whenever delivery semantics or scope change: the selector, `reconciliation_mode`, destination, `scope`, `exclude_account_ids`, or `operator_domains`. Each version has its own `accepted_at`. `active` and `replace_account_configs` change in place. Changes are **cutovers** at the instant they take effect, T. The outgoing schedule owes its periods that start before T, in full, ending at E. The incoming schedule owes its periods that start at or after E. No period is owed twice. When the two schedules' boundaries don't align, the stretch from E to the incoming schedule's next boundary is owed under neither; it is shorter than one incoming period. Version changes (for accounts in both versions, T is the new `accepted_at`), deactivation, overrides, `replace_account_configs`, offering resolution changes, and account timezone changes all use this rule. When the two sides resolve different offerings, E is computed per incoming offering: an offering that continues across T hands off at the end of its own last period started before T; a new offering starts at the latest such end across the outgoing offerings. Reactivating an inactive subscription is a fresh start at the reactivation instant, and nothing starting while it was inactive is owed; the readback lists each interval in `inactive_intervals`. See [cutovers](/docs/media-buy/task-reference/get_reporting_status#which-periods-are-owed).

Obligations from a subscription carry `delivery_config_id` equal to `subscription_id` and `delivery_config_version` equal to `subscription_version`. Subscription IDs and the principal's `sync_accounts` `delivery_config_id` values therefore share one namespace, and the seller rejects a collision. A `subscription_id` is never `.` or `..`.

Expand All @@ -355,7 +357,7 @@ To end its own account-level overrides without calling `sync_accounts`, a princi

**Exceptions without polling.** [`get_principal`](/docs/protocol/get_principal) lists, per subscription, `accounts_with_exceptions[]`: each account with an open exception and its reasons. An agency doesn't have to poll `get_reporting_status` account by account. `principal.changed` with reason `reporting_subscription_changed` fires whenever an account's exception opens or closes.

**Lost authority.** When the principal loses authority for an account the subscription covered, `get_principal` lists it under that subscription's `accounts_unauthorized[]` with `unauthorized_at`, `last_owed_period_end`, and the count of withheld periods. The entry stays until re-grant or for `status_retention_days`. After that it drops, and a later re-grant is a fresh coverage start from the new `authorized_at`, with no gap claimed. Each addition or removal fires `principal.changed` with reason `reporting_subscription_changed`, as does a subscription moving to `action_required`. [`get_reporting_status`](/docs/media-buy/task-reference/get_reporting_status) treats the account as unknown, as for any unauthorized caller.
**Lost authority.** When the principal loses authority for an account the subscription covered, `get_principal` lists it under that subscription's `accounts_unauthorized[]` with `unauthorized_at`, `last_owed_period_end`, and the count of withheld periods. The entry stays until re-grant or for `status_retention_days`. A re-grant before that continues the earlier grant: `authorized_at` stays at the first grant and withheld periods are owed again. After that the entry drops, and a later re-grant is a fresh coverage start from the new `authorized_at`, with no gap claimed. Each addition or removal fires `principal.changed` with reason `reporting_subscription_changed`, as does a subscription moving to `action_required`. [`get_reporting_status`](/docs/media-buy/task-reference/get_reporting_status) treats the account as unknown, as for any unauthorized caller.

How coverage starts and ends, and what happens on revocation and re-grant, is in [`get_reporting_status`](/docs/media-buy/task-reference/get_reporting_status#subscription-coverage).

Expand Down
Loading
Loading