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
10 changes: 10 additions & 0 deletions .changeset/account-request-money-terms-and-timezone.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"adcontextprotocol": minor
---

Account requests (experimental): resolved requests carry money terms, money changes say when they take effect, and timezone changeability is consistent.

- A request that resolves as `matched` or `created` now MUST carry `account`, with the account's `billing`, `billing_entity` (without write-only fields) and `payment_terms`, because they can differ from the terms requested.
- A request that resolves as `updated` and changed `billing`, `billing_entity` or `payment_terms` carries `effective_at`, when the new money terms take effect.
- A change request that resolves as `created` (a separate account) carries `not_carried_over`: the account-level configurations the new account doesn't have.
- `currency` and `timezone` change through `request_account` only on sellers that list them in `account.requests.changeable_attributes`; a seller that changes timezone MUST list it. `request_account` and `sync_principal` docs now agree.
23 changes: 15 additions & 8 deletions docs/accounts/tasks/request_account.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Call it only when [`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities
| `state` | Carries | What the buyer does |
| --- | --- | --- |
| `in_review` | `pending_action` | `waiting_on: seller`: wait for `account_request.changed` or poll [`list_accounts`](/docs/accounts/tasks/list_accounts) with `request_ids`. `waiting_on: buyer`: show `pending_action.url` to a human, then keep waiting. |
| `resolved` | `account_id`, `resolution` (`matched`, `created`, or `updated`), and for a change the updated `account` | Store `account_id` and send it on every account-scoped task. |
| `resolved` | `account_id`, `resolution` (`matched`, `created`, or `updated`), and `account`: always for `matched` and `created`, with the account's money terms, and for a change the updated account | Store `account_id` and send it on every account-scoped task. Check the money terms in `account`: they can differ from the ones requested. |
| `ambiguous` | `candidates[]` | A human picks one; call `request_account` with that `account_id` and the open `request_id`, and the request resolves as `matched`. |
| `rejected` | `errors[]` | Tell a human. A new request for the same key opens a new request. |

Expand Down Expand Up @@ -74,7 +74,7 @@ Send either `account_id` (to change or select), or `brand` and `operator` (to op

## Changing an account

Send the `account_id` and only the attributes to change. The changeable attributes are those [#8474](https://github.com/adcontextprotocol/adcp/pull/8474) defines: the brand's `countries`, `operator_unit`, `billing`, `billing_entity`, and `payment_terms`. `currency` and `timezone` are set when the account opens, and a seller MAY refuse to change them; `sandbox` never changes. Set `allow_separate_account: true` if you would accept the change on a separate new account when the seller can't make it on this one; it defaults to `false`.
Send the `account_id` and only the attributes to change. The seller lists the attributes it changes in `account.requests.changeable_attributes`, from the brand's `countries`, `operator_unit`, `billing`, `billing_entity`, `payment_terms`, `currency`, and `timezone`. `currency` and `timezone` are set when the account opens and change only on a seller that lists them; anywhere else the change is refused with [`UNSUPPORTED_FEATURE`](/docs/building/verification/compliance-catalog#error-code-unsupported-feature). A timezone change is a reporting cutover: account-timezone periods switch at the first old-timezone boundary at or after it, as [reporting subscriptions](/docs/protocol/sync_principal#reporting-subscriptions) describe. `sandbox` never changes. Set `allow_separate_account: true` if you would accept the change on a separate new account when the seller can't make it on this one; it defaults to `false`.

```json
{
Expand All @@ -88,12 +88,12 @@ Send the `account_id` and only the attributes to change. The changeable attribut

The seller validates a changed attribute as it does when the account opens, including authorization such as the brand-operator check. Then one of:

- **It applies the change:** `state: resolved`, `resolution: updated`, and `account` carrying the updated attributes and new `revision`.
- **It applies the change:** `state: resolved`, `resolution: updated`, and `account` carrying the updated attributes and new `revision`. When the change touches `billing`, `billing_entity`, or `payment_terms`, the request also carries `effective_at`, when the new money terms take effect: delivery before it is invoiced under the old terms. The seller may pick a later instant than resolution, such as its next invoice-period boundary. Other attributes change when the request resolves.
- **It reviews the change first:** `state: in_review` with a pending action. The account keeps its current attributes until the request resolves.
- **It refuses the change:** `state: rejected` with the reason. The account is unchanged.
- **It opens a separate account instead, if the buyer allowed it:** `state: resolved`, `resolution: created`, and a new `account_id`, with `account` carrying the new account. The seller says "yes, but as a separate account", for example when a new market needs its own contract or invoicing entity. The seller may do this only when the request set `allow_separate_account: true`. The new account has the named account's attributes with the requested change applied. Terms the seller needs to set differently go through a pending action before the request resolves.
- **It opens a separate account instead, if the buyer allowed it:** `state: resolved`, `resolution: created`, and a new `account_id`, with `account` carrying the new account. The seller says "yes, but as a separate account", for example when a new market needs its own contract or invoicing entity. The seller may do this only when the request set `allow_separate_account: true`. The new account has the named account's attributes with the requested change applied, and `account` carries its money terms. Terms the seller needs to set differently go through a pending action before the request resolves. The request's `not_carried_over` lists the named account's account-level configurations the new account doesn't have, from `notification_configs`, `reporting_delivery_configs`, `reporting_bucket`, `governance_agents`, `creatives`, `audiences`, `catalogs`, and `event_sources`; `[]` means all of them carried over. Principal-level configuration is unaffected: an `all_authorized_accounts` reporting subscription covers the new account once it is authorized, while an `account_ids` subscription covers it only after you add it.
- **It would accept the change only as a separate account, and the buyer didn't allow that:** `state: rejected` with [`ACCOUNT_CHANGE_REQUIRES_NEW_ACCOUNT`](/docs/building/verification/compliance-catalog#error-code-account-change-requires-new-account). The account is unchanged. The buyer can tell this from a plain refusal: it may open a new account itself, resend the change with `allow_separate_account: true`, or keep the account as it is.
- **It points to an account the caller already holds:** `state: resolved`, `resolution: matched`, and that account's `account_id`. The seller uses this when the change would make the named account duplicate an account the caller can already reach; see [Accounts that end up alike](/docs/accounts/overview#account-identity). As with `candidates`, the seller MUST NOT point to an account outside the caller's grants.
- **It points to an account the caller already holds:** `state: resolved`, `resolution: matched`, and that account's `account_id`, with `account` carrying its money terms. The seller uses this when the change would make the named account duplicate an account the caller can already reach; see [Accounts that end up alike](/docs/accounts/overview#account-identity). As with `candidates`, the seller MUST NOT point to an account outside the caller's grants.

When the request resolves to another account (`created` or `matched`), the named account is untouched: it keeps its attributes, media buys, and reporting history, and stays usable. The seller MUST NOT move media buys, creatives, or reporting between the two accounts. Which account carries new buys is the buyer's choice. A buyer can tell these outcomes apart because the resolved `account_id` differs from `requested.account_id`.

Expand Down Expand Up @@ -148,14 +148,21 @@ A day later, sales ops finds no relationship and hands the next step to the buye
}
```

Once the human registers, the request resolves:
Once the human registers, the request resolves. It carries `account` with the new account's money terms, because they can differ from the ones requested (`requested`, `created_at`, and `updated_at` are omitted here):

```json
{
"request_id": "areq_01K4F8M2Q7",
"state": "resolved",
"account_id": "acc_renault_fr_wpp",
"resolution": "created"
"resolution": "created",
"account": {
"account_id": "acc_renault_fr_wpp",
"name": "Renault France via WPP Media",
"status": "active",
"billing": "operator",
"payment_terms": "net_45"
}
}
```

Expand Down Expand Up @@ -227,4 +234,4 @@ A seller that does deduplicate typically answers a repeated *open* request as fo
| [`INVALID_REQUEST`](/docs/building/verification/compliance-catalog#error-code-invalid-request) (money change) | A change to `billing`, `billing_entity`, or `payment_terms` without `revision`. |
| [`BILLING_NOT_SUPPORTED`](/docs/building/verification/compliance-catalog#error-code-billing-not-supported) | The requested `billing` isn't in the seller's `supported_billing`. Sellers MAY instead record the request and reject it. |
| [`IDEMPOTENCY_CONFLICT`](/docs/building/verification/compliance-catalog#error-code-idempotency-conflict) | The `idempotency_key` was replayed with a different body. |
| [`UNSUPPORTED_FEATURE`](/docs/building/verification/compliance-catalog#error-code-unsupported-feature) | The seller doesn't support account requests, or refuses to change `currency`, `timezone`, or `sandbox` on an existing account. |
| [`UNSUPPORTED_FEATURE`](/docs/building/verification/compliance-catalog#error-code-unsupported-feature) | The seller doesn't support account requests; or a change names `currency` or `timezone` and the seller doesn't list it in `changeable_attributes`; or a change names `sandbox`. |
2 changes: 1 addition & 1 deletion docs/protocol/get_adcp_capabilities.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,7 @@ advertising `supported: true` MUST also list `account.requests` in

`event_type` is present only when the seller fires `account_request.changed`; a seller that declares it also declares `adcp.capability_changes.notifications.supported: true`, because buyers subscribe through the caller-scoped `notification_configs`. Without it, buyers poll `list_accounts` with `request_ids`.

`changeable_attributes` lists the attributes the seller accepts in `request_account` change requests (`account_id` plus fields); absent or empty means the seller supports opening accounts only.
`changeable_attributes` lists the attributes the seller accepts in `request_account` change requests (`account_id` plus fields); absent or empty means the seller supports opening accounts only. `currency` and `timezone` change only when listed: a seller that changes an account's timezone through `request_account` lists `timezone`, and refuses an unlisted `currency` or `timezone` change with [`UNSUPPORTED_FEATURE`](/docs/building/verification/compliance-catalog#error-code-unsupported-feature).

`pending_action_kinds` is advisory: it lets buyers prepare the people who will
act. Sellers may issue other kinds, and buyers handle unknown kinds through the
Expand Down
2 changes: 1 addition & 1 deletion docs/protocol/sync_principal.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ For example, if a seller offers `package`, `package × creative`, and `package

`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 package of a covered buy, against that package's product. A package resolves when its buy and subscription version first meet, at the later of the version's `accepted_at` and the buy's `confirmed_at`, and keeps that offering for the version until the seller withdraws it. A later offering never re-resolves it; a new version resolves afresh. The buy owes every offering any of its packages resolves, so even under `exact` or `finest` a buy whose packages come from different products can owe two offerings, each with its own periods. Each package on `get_media_buys` carries its own `reporting_delivery_offering_ids`, fixed when the package is confirmed, and each offering publishes `available_from` and `withdrawn_at`, so a buyer resolves every package from the wire. 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. 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. Sub-daily periods under a local alignment restart at each local midnight instead: hourly periods start on the local hour in every zone, a 23-hour DST day has 23 of them and a 25-hour day 25, and the duration must divide 24 hours. Each local date's first instant is always a boundary, even where midnight doesn't exist (Havana, Santiago, Cairo), so days nest in every sub-daily schedule and `PT24H` equals `P1D`. A 30-minute DST change, as on Lord Howe Island, makes one hourly period 90 minutes long. Weeks (`P7D`) under `utc`, `account_timezone`, and `source_timezone` start on Thursday, the weekday of 1970-01-01; `billing_cycle` with an anchor gives another week start, and `billing_cycle` always needs a date component. 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. Sub-daily periods under a local alignment restart at each local midnight instead: hourly periods start on the local hour in every zone, a 23-hour DST day has 23 of them and a 25-hour day 25, and the duration must divide 24 hours. Each local date's first instant is always a boundary, even where midnight doesn't exist (Havana, Santiago, Cairo), so days nest in every sub-daily schedule and `PT24H` equals `P1D`. A 30-minute DST change, as on Lord Howe Island, makes one hourly period 90 minutes long. Weeks (`P7D`) under `utc`, `account_timezone`, and `source_timezone` start on Thursday, the weekday of 1970-01-01; `billing_cycle` with an anchor gives another week start, and `billing_cycle` always needs a date component. When an account's timezone changes, for example through a [`request_account`](/docs/accounts/tasks/request_account) change on a seller that lists `timezone` in `account.requests.changeable_attributes`, 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`, so a seller advertising subscriptions publishes every managed-delivery offering with a row schema that declares `account_id` as a required column and lists it in `primary_keys`. 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, `destination_overrides[]` routes some accounts, by `account_ids` or `operator_domains`, to another destination inside the same subscription; an `account_ids` entry wins over an `operator_domains` one, and every override destination has the same pattern and transport as `destination`. An account-scoped subscription pointing at its own destination also works, and sellers that support subscriptions MUST support that. Changing overrides needs a new version, like any destination change.

Expand Down
Loading
Loading